nix-nar 0.5.0

Library to manipulate Nix Archive (nar) files
Documentation
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use std::{fs, io};

use camino::{Utf8Path, Utf8PathBuf};
#[cfg(not(unix))]
use is_executable::IsExecutable;

#[derive(Debug, Clone)]
pub enum FileSystemMetadata {
    Directory,
    File { executable: bool, length: u64 },
    Symlink { target_path: Utf8PathBuf },
}

/// File system operations needed by [`crate::Encoder`] to encode a NAR file.
///
/// [`crate::Encoder`] will call this trait's methods only with `path`s that are:
/// - The archive's root (given to the [`crate::Encoder`]'s constructor), or
/// - Constructed by appending [`Self::read_dir`] iterator's items to
///   the archive's root to descend into directories
///
/// # Consistency
///
/// Implementation is responsible for returning a consistent view of the filesystem.
///
/// The reader returned by [`Self::open`] must read exactly the number
/// of bytes that was declared in its corresponding
/// [`Self::metadata`]. In case of a mismatch, [`crate::Encoder`] will
/// return an error to avoid creating an invalid NAR file.
pub trait FileSystem {
    /// Reader returned by [`Self::open`]
    type File: io::Read;

    /// Return type of a filesystem object.
    ///
    /// # Errors
    ///
    /// May return an I/O error.
    ///
    /// Implementation should return an error (or avoid listing that
    /// object in [`Self::read_dir`]) if the object is not one of the
    /// types enumerated in [`FileSystemMetadata`].
    fn metadata(&self, path: &Utf8Path) -> io::Result<FileSystemMetadata>;

    /// Iterate over entry names of a directory at the given path.
    ///
    /// [`crate::Encoder`] will call this method only on paths, for
    /// which [`Self::metadata`] returned [`FileSystemMetadata::Directory`].
    ///
    /// # Errors
    ///
    /// May return an I/O error
    fn read_dir(&self, path: &Utf8Path) -> io::Result<Vec<String>>;

    /// Open a file at the given path, return its size in bytes and
    /// a [`Self::File`] reader
    ///
    /// [`crate::Encoder`] will call this method only on paths for
    /// which [`Self::metadata`] returned [`FileSystemMetadata::File`].
    ///
    /// # Errors
    ///
    /// May return an I/O error
    fn open(&self, path: &Utf8Path) -> io::Result<Self::File>;
}

/// Default implementation of the [`FileSystem`] trait, using the OS
/// filesystem directly
#[derive(Debug, Clone, Copy, Default)]
pub struct NativeFileSystem {}

impl FileSystem for NativeFileSystem {
    type File = fs::File;

    fn open(&self, path: &Utf8Path) -> io::Result<Self::File> {
        fs::File::open(path)
    }

    fn read_dir(&self, path: &Utf8Path) -> io::Result<Vec<String>> {
        path.read_dir_utf8()?
            .map(|i| i.map(|p| p.file_name().into()))
            .collect()
    }

    fn metadata(&self, path: &Utf8Path) -> io::Result<FileSystemMetadata> {
        let metadata = path.symlink_metadata()?;
        if metadata.is_dir() {
            Ok(FileSystemMetadata::Directory)
        } else if metadata.is_symlink() {
            Ok(FileSystemMetadata::Symlink {
                target_path: path.read_link_utf8()?,
            })
        } else if metadata.is_file() {
            // On Unix, we can reuse existing metadata, saving a syscall
            #[cfg(unix)]
            let executable = metadata.mode() & 0o111 != 0;
            // On non-Unix, defer to is_executable crate
            #[cfg(not(unix))]
            let executable = path.as_std_path().is_executable();
            Ok(FileSystemMetadata::File {
                executable,
                length: metadata.len(),
            })
        } else {
            Err(io::Error::other(format!("unknown file type {path}")))
        }
    }
}