xzip 0.4.1

ZIP CLI with explicit filename encoding for pack and unpack workflows.
Documentation
use std::fs::{self, File, OpenOptions};
use std::path::Path;

use crate::error::XzipError;

/// Open a zip archive for reading with platform-specific tuning.
pub fn open_zip_file(path: &Path) -> Result<File, XzipError> {
    let mut options = OpenOptions::new();
    options.read(true);

    #[cfg(windows)]
    {
        use std::os::windows::fs::OpenOptionsExt;
        const FILE_SHARE_READ: u32 = 0x0000_0001;
        const FILE_SHARE_DELETE: u32 = 0x0000_0004;
        const FILE_FLAG_SEQUENTIAL_SCAN: u32 = 0x0800_0000;
        options.share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE);
        options.custom_flags(FILE_FLAG_SEQUENTIAL_SCAN);
    }

    options.open(path).map_err(XzipError::from)
}

/// Create a parent directory if needed and apply platform-specific directory hints.
pub fn create_dir(path: &Path) -> Result<(), XzipError> {
    if path.as_os_str().is_empty() {
        return Err(XzipError::InvalidInput(path.to_path_buf()));
    }
    if path.is_dir() {
        return Ok(());
    }
    fs::create_dir(path)?;
    apply_dir_hints(path);
    Ok(())
}

/// Create a directory tree if needed and apply platform-specific directory hints.
pub fn create_dir_all(path: &Path) -> Result<(), XzipError> {
    if path.as_os_str().is_empty() {
        return Err(XzipError::InvalidInput(path.to_path_buf()));
    }
    if path.is_dir() {
        return Ok(());
    }
    fs::create_dir_all(path)?;
    apply_dir_hints(path);
    Ok(())
}

/// Create an output file for extraction.
pub fn create_output_file(path: &Path) -> Result<File, XzipError> {
    if let Some(parent) = path.parent() {
        create_dir_all(parent)?;
    }
    File::create(path).map_err(XzipError::from)
}

#[cfg(windows)]
fn apply_dir_hints(path: &Path) {
    use std::os::windows::ffi::OsStrExt;

    const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x0000_2000;

    let Ok(path) = path.canonicalize() else {
        return;
    };

    let wide: Vec<u16> = path
        .as_os_str()
        .encode_wide()
        .chain(std::iter::once(0))
        .collect();

    let attrs =
        unsafe { windows_sys::Win32::Storage::FileSystem::GetFileAttributesW(wide.as_ptr()) };
    if attrs == u32::MAX {
        return;
    }

    let updated = attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED;
    let _ = unsafe {
        windows_sys::Win32::Storage::FileSystem::SetFileAttributesW(wide.as_ptr(), updated)
    };
}

#[cfg(not(windows))]
fn apply_dir_hints(_path: &Path) {}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::{create_dir, create_dir_all};

    #[test]
    fn create_dir_all_makes_nested_tree() {
        let temp = tempdir().expect("temp dir");
        let nested = temp.path().join("a/b/c");
        create_dir_all(&nested).expect("create nested dir");
        assert!(nested.is_dir());
    }

    #[test]
    fn create_dir_skips_existing_directory() {
        let temp = tempdir().expect("temp dir");
        let dir = temp.path().join("exists");
        fs::create_dir(&dir).expect("create dir");
        create_dir(&dir).expect("existing dir should be ok");
    }
}