ocinoco 0.1.0

Build OCI image with no container
use std::io::Result;
use std::path::{Path, PathBuf};

use rayon::iter::{ParallelBridge, ParallelIterator};

use crate::fs::{DirEntry, File, Source, Symlink};

#[cfg(unix)]
use std::os::unix::fs::MetadataExt;

pub(crate) struct OsSource {
    root_dir: PathBuf,
}

impl OsSource {
    pub(crate) fn new(root_dir: impl AsRef<Path>) -> Self {
        Self {
            root_dir: root_dir.as_ref().to_path_buf(),
        }
    }
}

impl Source for OsSource {
    fn read_dir(&self, path: &Path) -> Result<impl ParallelIterator<Item = DirEntry>> {
        let root_dir = self.root_dir.clone();

        Ok(std::fs::read_dir(self.resolve(path))?
            .par_bridge()
            .filter_map(move |entry| {
                let entry = entry.ok()?;
                let file_type = entry.file_type().ok()?;
                let path = entry.path().strip_prefix(&root_dir).ok()?.to_path_buf();

                if file_type.is_symlink() {
                    Some(DirEntry::Symlink(Symlink {
                        path,
                        target: entry.path().read_link().ok()?,
                        #[cfg(unix)]
                        mode: entry.metadata().ok()?.mode(),
                        #[cfg(not(unix))]
                        mode: 644,
                    }))
                } else if file_type.is_file() {
                    Some(DirEntry::File(File {
                        path,
                        #[cfg(unix)]
                        mode: entry.metadata().ok()?.mode(),
                        #[cfg(not(unix))]
                        mode: 644,
                    }))
                } else if file_type.is_dir() {
                    Some(DirEntry::Directory(path))
                } else {
                    None
                }
            }))
    }

    fn read(&self, path: &Path) -> Result<Vec<u8>> {
        std::fs::read(self.resolve(path))
    }
}

impl OsSource {
    fn resolve(&self, path: &Path) -> PathBuf {
        self.root_dir.join(path.strip_prefix("/").unwrap_or(path))
    }
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::time::{SystemTime, UNIX_EPOCH};

    use rayon::iter::ParallelIterator;

    use super::*;

    fn temp_root() -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();

        std::env::temp_dir().join(format!("ocinoco-os-source-{nanos}"))
    }

    #[test]
    fn reads_paths_under_root_dir_even_when_path_is_absolute() {
        let root = temp_root();
        fs::create_dir_all(root.join("nested")).unwrap();
        fs::write(root.join("nested/file.txt"), b"content").unwrap();
        fs::write(root.join("root.txt"), b"root").unwrap();

        let source = OsSource::new(&root);
        let mut entries = source.read_dir(Path::new("/")).unwrap().collect::<Vec<_>>();

        entries.sort_by_key(|entry| match entry {
            DirEntry::Directory(path) => path.clone(),
            DirEntry::File(file) => file.path.clone(),
            DirEntry::Symlink(symlink) => symlink.path.clone(),
        });

        assert!(entries.iter().any(|entry| {
            matches!(entry, DirEntry::Directory(path) if path == Path::new("nested"))
        }));
        assert!(entries.iter().any(|entry| {
            matches!(entry, DirEntry::File(file) if file.path == Path::new("root.txt"))
        }));
        assert_eq!(
            source.read(Path::new("/nested/file.txt")).unwrap(),
            b"content"
        );

        fs::remove_dir_all(root).unwrap();
    }

    #[test]
    fn resolves_relative_paths_under_root_dir() {
        let root = temp_root();
        let source = OsSource::new(&root);

        assert_eq!(source.resolve(Path::new("/a/b")), root.join("a/b"));
        assert_eq!(source.resolve(Path::new("a/b")), root.join("a/b"));
    }
}