asar-rust 0.1.0

Rust port of @electron/asar — create and extract Electron ASAR archives
Documentation
use crate::disk::AsarError;
use glob::glob;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Metadata for a file discovered during filesystem crawling.
#[derive(Debug, Clone)]
pub struct FileMetadata {
    pub file_type: FileType,
    pub mode: u32,
    pub size: u64,
}

/// The type of a filesystem entry discovered during crawling.
#[derive(Debug, Clone, PartialEq)]
pub enum FileType {
    File,
    Directory,
    Link,
}

/// Collect every path whose `FileType` is `Link` from the metadata map.
fn links_in(metadata: &HashMap<PathBuf, FileMetadata>) -> Option<Vec<PathBuf>> {
    let links: Vec<_> = metadata
        .iter()
        .filter_map(|(p, m)| (m.file_type == FileType::Link).then_some(p.clone()))
        .collect();
    if links.is_empty() {
        None
    } else {
        Some(links)
    }
}

pub fn crawl(pattern: &str) -> Result<(Vec<PathBuf>, HashMap<PathBuf, FileMetadata>), AsarError> {
    let mut metadata: HashMap<PathBuf, FileMetadata> = HashMap::new();
    let mut paths = Vec::new();

    let entries = glob(pattern).map_err(|e| AsarError::Other(e.to_string()))?;

    for entry in entries {
        let path = entry.map_err(|e| AsarError::Other(e.to_string()))?;
        if let Some(ft) = determine_file_type(&path) {
            let meta = fs::symlink_metadata(&path).map_err(|e| AsarError::Other(e.to_string()))?;
            let mode = {
                #[cfg(unix)]
                {
                    if let Ok(regular_meta) = fs::metadata(&path) {
                        use std::os::unix::fs::PermissionsExt;
                        regular_meta.permissions().mode()
                    } else {
                        0o644
                    }
                }
                #[cfg(not(unix))]
                {
                    0o644
                }
            };

            metadata.insert(
                path.clone(),
                FileMetadata {
                    file_type: ft,
                    mode,
                    size: meta.len(),
                },
            );
            paths.push(path);
        }
    }

    paths.sort_unstable();

    // Exclude files whose ancestor directory is a symlink.
    let paths = if let Some(link_dirs) = links_in(&metadata) {
        let link_set: std::collections::HashSet<_> = link_dirs.iter().cloned().collect();
        paths
            .into_iter()
            .filter(|filename| {
                if link_set.contains(filename) {
                    return true;
                }
                let mut ancestor = filename.as_path();
                while let Some(parent) = ancestor.parent() {
                    if parent.as_os_str().is_empty() {
                        break;
                    }
                    if link_set.contains(parent) {
                        return false;
                    }
                    ancestor = parent;
                }
                true
            })
            .collect()
    } else {
        paths
    };

    Ok((paths, metadata))
}

fn determine_file_type(path: &Path) -> Option<FileType> {
    let meta = fs::symlink_metadata(path).ok()?;
    let file_type = meta.file_type();
    if file_type.is_file() {
        Some(FileType::File)
    } else if file_type.is_dir() {
        Some(FileType::Directory)
    } else if file_type.is_symlink() {
        Some(FileType::Link)
    } else {
        None
    }
}