https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119289

            Bug ID: 119289
           Summary: Incorrect behavior of std::filesystem::copy() with
                    none options and the destination link to non-existent
                    file
           Product: gcc
           Version: 14.2.0
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: parkjuny at kaist dot ac.kr
  Target Milestone: ---

## Versions and Compile Options

Checked on gcc 13.3.0 and 14.2.0, exact version strings are as follows:
- `g++-14 (Ubuntu 14.2.0-4ubuntu2~24.04) 14.2.0`
- `g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0`

Compile option I used is: `g++ test.cpp -o test -Wall -Wextra`. I also tried
`-std=c++17` and `-std=c++20`, but the results were the same.

## Summary

`std::filesystem::copy` with no symbolic link options and `to` is a symlink to
a non-existent file currently reports an error (file already exists). However,
this is expected to correctly copy the contents of the source file to the path
to which `to` resolves.

## Reproducing

### Files

I prepared two files:
- `src.txt`: a regular file with some random contents
- `link_dst`: a symlink to a non-existent file `asdf`

Copy would be attempted from `src.txt` to the path to which `link_dst`
resolves, the non-existent file `asdf`.

```
candymate@server:~/temp$ ls -al
total 56
drwxrwxr-x 2 candymate candymate  4096  3월 14 21:13 .
drwxr-xr-x 9 candymate candymate  4096  3월 14 21:05 ..
lrwxrwxrwx 1 candymate candymate    25  3월 14 21:00 link_dst ->
/home/candymate/temp/asdf
-rw-rw-r-- 1 candymate candymate     5  3월 14 12:54 src.txt
-rwxrwxr-x 1 candymate candymate 39464  3월 14 21:13 test
-rw-rw-r-- 1 candymate candymate  1804  3월 14 21:05 test.cpp
```

### Code

```cpp
#include <fstream>
#include <filesystem>
#include <iostream>

namespace fs = std::filesystem;

void demo_status(fs::file_status s)
{
    switch (s.type())
    {
        case fs::file_type::none:
            std::cout << " has `not-evaluated-yet` type";
            break;
        case fs::file_type::not_found:
            std::cout << " does not exist";
            break;
        case fs::file_type::regular:
            std::cout << " is a regular file";
            break;
        case fs::file_type::directory:
            std::cout << " is a directory";
            break;
        case fs::file_type::symlink:
            std::cout << " is a symlink";
            break;
        case fs::file_type::block:
            std::cout << " is a block device";
            break;
        case fs::file_type::character:
            std::cout << " is a character device";
            break;
        case fs::file_type::fifo:
            std::cout << " is a named IPC pipe";
            break;
        case fs::file_type::socket:
            std::cout << " is a named IPC socket";
            break;
        case fs::file_type::unknown:
            std::cout << " has `unknown` type";
            break;
        default:
            std::cout << " has `implementation-defined` type";
            break;
    }
    std::cout << '\n';
}

int main()
{
    // to check control values
    std::cout << std::filesystem::exists("link_dst") << std::endl;
    demo_status(std::filesystem::status("link_dst"));

    std::filesystem::copy("src.txt", "link_dst"); // this does not work (throws
error)

    // this works by correctly copying source src.txt to the destination
link_dst is pointing to
    //std::filesystem::copy("src.txt", "link_dst",
std::filesystem::copy_options::overwrite_existing);
    return 0;
}
```

### Build and Run

```bash
g++ test.cpp -o test -Wall -Wextra
./test
```

### Expected

```
0
 does not exist
```

```
total 44
drwxrwxr-x 2 candymate candymate  4096  3월 14 21:02 ./
drwxr-xr-x 9 candymate candymate  4096  3월 14 21:02 ../
-rw-rw-r-- 1 candymate candymate     5  3월 14 21:02 asdf
lrwxrwxrwx 1 candymate candymate    25  3월 14 21:00 link_dst ->
/home/candymate/temp/asdf
-rw-rw-r-- 1 candymate candymate     5  3월 14 12:54 src.txt
-rwxrwxr-x 1 candymate candymate 21880  3월 14 21:02 test*
-rw-rw-r-- 1 candymate candymate  1742  3월 14 21:02 test.cpp
```

### Actual

```
0
 does not exist
terminate called after throwing an instance of
'std::filesystem::__cxx11::filesystem_error'
  what():  filesystem error: cannot copy: File exists [src.txt] [link_dst]
Aborted (core dumped)
```

No changes in the file system.

### Analysis on Specification

I've checked the draft version of the specification for the accurate
description of the behavior of `std::filesystem::copy`.
(https://github.com/cplusplus/draft)

- `std::filesystem::copy`
    - (4.1) If `create_symlinks` or `skip_symlinks` is present, ... --
**false** --> (4.2)
    - (4.2) Otherwise, if `copy_symlinks` is present, ... -- **false** -->
(4.3)
    - (4.3) Otherwise, `auto f = status(from)` and `auto t = status(to)` --
**true** --> (4.4)
        - This gives `t.type() == fs::file_type::not_found`
    - (4.4) If `f.type()` or `t.type()` is an implementation-defined file type,
... -- **false** --> (4.5)
    - (4.5) Otherwise, an error is reported as specified in 31.12.5 if
        - (4.5.1) `exists(f)` is false -- **false** --> (4.5.2)
        - (4.5.2) `equivalent(from, to)` is true -- **false** --> (4.5.3)
        - (4.5.3) `is_other(f) || is_other(t)` is true -- **false** --> (4.5.4)
            - `exists(t)` is false, so `is_other(t)` is false
        - (4.5.4) `is_directory(f) && is_regular_file(t)` is true -- **false**
--> (4.6)
    - (4.6) Otherwise, if `is_symlink(f)` -- **false** --> (4.7)
    - (4.7) Otherwise, if `is_regular_file(f)`, then -- **true**
        - (4.7.1) `directories_only` is present -- **false** --> (4.7.2)
        - (4.7.2) `create_symlinks` is present -- **false** --> (4.7.3)
        - (4.7.3) `create_hard_links` is present -- **false** --> (4.7.4)
        - (4.7.4) `is_directory(t)` -- **false** --> (4.7.5)
        - **(4.7.5) Otherwise, `copy_file(from, to, options)`** -- **true**

- `std::filesystem::copy_file`
    - (4.1) Report an error as specified in 31.12.5 if -- **false**
        - (4.1.1) `is_regular_file(from)` is false -- **false** --> (4.1.2)
        - (4.1.2-4.1.4) `exists(to)` is true -- **false** --> (4.2)
    - (4.2) Otherwise, copy the contents and attributes of the file `from`
resolves to, to the file `to` resolves to, if -- **true**
        - **(4.2.1) `exists(to)` is false** -- **true**

Therefore, `std::filesystem::copy` should correctly copy the contents of the
source file to the path to which `to` resolves.

I also experimented with `overwrite_existing` option, and it works as expected.
It correctly copies the contents of the source file to the destination path the
link is pointing to.

### Etc

- Due to my misunderstanding to the specification and the report rules, I once
wrongly posted a bug report as https://github.com/cplusplus/draft/issues/7734.
I'm sorry for this. **This report is different from the previous one.**
- I've rechecked the specification three times, and I'm pretty sure that my
understanding is correct. I hope I don't have any other misunderstandings.

Reply via email to