Rust でファイルコピーを行う関数 std::fs::copy について、 macOS と Linux で挙動の違いがあることに気づいたため、まとめます。

TL;DR

std::fs::copy は、Linux においてはファイルの中身およびパーミッションだけコピーを行います。メタデータ( Access Control List (ACL) やファイルの変更時間など)はコピーされません。

一方、 macOS においては、ファイルの中身に加えて、メタデータも複製されます

std::fs::copy の実装を見てみる

std::fs::copy の内部実装は OS ごとに分かれています。

Linux

Linux の実装を見てみると、以下のようになっています(抜粋、一部筆者のコメント付き)。

let (mut reader, reader_metadata) = open_from(from)?;
let len = reader_metadata.len();

// パーミッションをセットしてそう
let (mut writer, _) = open_to_and_set_permissions(to, reader_metadata)?;

let has_copy_file_range = HAS_COPY_FILE_RANGE.load(Ordering::Relaxed);
let mut written = 0u64;
// ここの while でファイルの中身をコピーしてそう
while written < len {
    let copy_result = if has_copy_file_range {
        let bytes_to_copy = cmp::min(len - written, usize::max_value() as u64) as usize;
        let copy_result = unsafe {
            cvt(copy_file_range(
                reader.as_raw_fd(),
                ptr::null_mut(),
                writer.as_raw_fd(),
                ptr::null_mut(),
                bytes_to_copy,
                0,
            ))
        };
        // 中略
        copy_result
    } else {
        Err(io::Error::from_raw_os_error(libc::ENOSYS))
    };
    // 中略
}
Ok(written)

reader から writerbytes_to_copy バイトずつ書き込む、という操作を、コピーが終わるまで繰り返す。というような処理になっていそうな感じです。

while の前にある open_to_and_set_permissions(to, reader_metadata) の箇所が気になります。実装を見てみます。

fn open_to_and_set_permissions(
    to: &Path,
    reader_metadata: crate::fs::Metadata,
) -> io::Result<(crate::fs::File, crate::fs::Metadata)> {
    use crate::fs::OpenOptions;
    use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt};

    let perm = reader_metadata.permissions();
    let writer = OpenOptions::new()
        .mode(perm.mode())
        .write(true)
        .create(true)
        .truncate(true)
        .open(to)?;
    let writer_metadata = writer.metadata()?;
    if writer_metadata.is_file() {
        writer.set_permissions(perm)?; // 読み取り元ファイルのパーミッションを読み取り先にセットしてる!
    }
    Ok((writer, writer_metadata))
}

実装は上のようになっていて、コメントにも書いた通り読み取り元のパーミッションを読み取り先にセットしていることが読み取れます。

以上のことから、Linux における std::fs::copy は、

  • ファイルの中身
  • パーミッション

の 2 つをコピーしているということが分かります。

macOS

では macOS 向けでの実装はどうなっているでしょうか。見てみます。

const COPYFILE_ACL: u32 = 1 << 0;
const COPYFILE_STAT: u32 = 1 << 1;
const COPYFILE_XATTR: u32 = 1 << 2;
const COPYFILE_DATA: u32 = 1 << 3;

const COPYFILE_SECURITY: u32 = COPYFILE_STAT | COPYFILE_ACL;
const COPYFILE_METADATA: u32 = COPYFILE_SECURITY | COPYFILE_XATTR;
const COPYFILE_ALL: u32 = COPYFILE_METADATA | COPYFILE_DATA;

// 中略

extern "C" {
    // macOS においてファイルコピーを行うための標準 C ライブラリ関数 `fcopyfile` を呼び出すためのインタフェース
    fn fcopyfile(
        from: libc::c_int,
        to: libc::c_int,
        state: copyfile_state_t,
        flags: copyfile_flags_t,
    ) -> libc::c_int;
}

// 中略

let flags = if writer_metadata.is_file() { COPYFILE_ALL } else { COPYFILE_DATA };
// `fcopyfile` を呼び出す
cvt(unsafe { fcopyfile(reader.as_raw_fd(), writer.as_raw_fd(), state.0, flags) })?;
// 後略

macOS にはファイルコピーを行うために libcfcopyfile という関数が用意されています。

Mac OS X Manual Page For copyfile(3)

この関数を Rust から呼び出すことでファイルコピーが実現されているようです。 fcopyfile に渡すフラグは、ファイルコピーであれば COPYFILE_ALL となります。 上記にリンクを貼った fcopyfile のドキュメントを見ると、 COPYFILE_ALL フラグを渡すと以下のコピーを行うことが分かります。

  • access control lists
  • POSIX information (mode, modification time, etc.)
  • extended attributes
  • data

つまり、メタデータ含めて全部コピー!!って感じですね。

まとめ

  • std::fs::copy は、 Linux では中身とパーミッションのみをコピーする
  • macOS ではメタデータも含めて全部コピーする
  • Linux では libc クレート 経由で直接システムコールを呼び出してコピーを行っているが、 macOS では OS の標準 C ライブラリにある fcopyfile 関数を呼び出すことでコピーを実現している

Rust の標準ライブラリ実装を読み解いていくのはとても楽しいし、勉強になるなと思いました 💪

今まであまり慣れ親しんでこなかった、システムコールを呼び出すほどの低レイヤー部分でもとても自然に読みすすめることができて、Rust の大きな特長の 1 つである zero-cost abstraction というか、低レイヤー部分でも適度な抽象度があって読みやすい、というような印象を持ちました。

今後も意欲的に Rust の標準ライブラリを読んでいったり、低レイヤー部分の知識を幅広く勉強していきたいなと思います。

なお、今回 std::fs::copy について調べたのは、このサイトの作成に使用している静的サイトジェネレータ Zola に PR を出すための調査を行ったからでした。

Preserve timestamps when copying files (#974) by magurotuna · Pull Request #983 · getzola/zola

merge されるかな〜とそわそわしています。

(2020/04/05 追記)

merge されました 🎉🎉 zola-983-merged

References