aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-archiver/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart-archiver/src/lib.rs')
-rw-r--r--crates/mozart-archiver/src/lib.rs899
1 files changed, 0 insertions, 899 deletions
diff --git a/crates/mozart-archiver/src/lib.rs b/crates/mozart-archiver/src/lib.rs
deleted file mode 100644
index 30c678a..0000000
--- a/crates/mozart-archiver/src/lib.rs
+++ /dev/null
@@ -1,899 +0,0 @@
-use anyhow::Context as _;
-use regex::Regex;
-use sha1::{Digest, Sha1};
-use std::fs;
-use std::io::Write as IoWrite;
-use std::path::{Path, PathBuf};
-
-pub mod manager;
-pub use manager::{ArchiveManager, ArchivePackage};
-
-/// A compiled exclude pattern derived from a gitignore-style rule.
-pub struct ExcludePattern {
- regex: Regex,
- /// If true, matching files are *re-included* (negation rule).
- negate: bool,
-}
-
-/// Convert a glob pattern string to a regex string.
-///
-/// Mapping:
-/// - `**` → `.*` (matches any path segment sequence)
-/// - `*` → `[^/]*` (matches within a single path segment)
-/// - `?` → `[^/]` (matches a single non-separator char)
-/// - `[…]` → `[…]` (character class, passed through)
-/// - all other characters are regex-escaped
-fn glob_to_regex(glob: &str) -> String {
- let mut result = String::new();
- let chars: Vec<char> = glob.chars().collect();
- let mut i = 0;
- while i < chars.len() {
- match chars[i] {
- '*' if i + 1 < chars.len() && chars[i + 1] == '*' => {
- result.push_str(".*");
- i += 2;
- }
- '*' => {
- result.push_str("[^/]*");
- i += 1;
- }
- '?' => {
- result.push_str("[^/]");
- i += 1;
- }
- '[' => {
- // Pass character classes through as-is until the closing `]`
- result.push('[');
- i += 1;
- while i < chars.len() && chars[i] != ']' {
- result.push(chars[i]);
- i += 1;
- }
- if i < chars.len() {
- result.push(']');
- i += 1;
- }
- }
- c => {
- // Regex-escape special characters
- if r"\.+^$|{}()?".contains(c) {
- result.push('\\');
- }
- result.push(c);
- i += 1;
- }
- }
- }
- result
-}
-
-/// Convert a single gitignore-style rule into an `ExcludePattern`.
-///
-/// Returns `None` if the rule is empty or a comment.
-pub fn parse_gitignore_pattern(rule: &str) -> Option<ExcludePattern> {
- let rule = rule.trim();
- if rule.is_empty() || rule.starts_with('#') {
- return None;
- }
-
- // Leading `!` negates the pattern
- let (negate, rule) = if let Some(rest) = rule.strip_prefix('!') {
- (true, rest)
- } else {
- (false, rule)
- };
-
- // Strip trailing `/` before globbing
- let rule = rule.trim_end_matches('/');
- if rule.is_empty() {
- return None;
- }
-
- // Determine anchor prefix:
- // - leading `/` → anchored at root: `^/<glob_regex>`
- // - no `/` inside pattern → matches anywhere: `/<glob_regex>`
- // - `/` somewhere in middle → anchored at root: `^/<glob_regex>`
- let (prefix, glob) = if let Some(without_leading_slash) = rule.strip_prefix('/') {
- // Root-anchored
- ("^/", without_leading_slash)
- } else if rule.contains('/') {
- // Slash in middle: treat as root-anchored
- ("^/", rule)
- } else {
- // No slash: matches anywhere
- ("/", rule)
- };
-
- let glob_regex = glob_to_regex(glob);
- // The final regex: `<prefix><glob_regex>(/|$)`
- // This matches the path component exactly (followed by a `/` or end-of-string).
- let pattern = format!("{prefix}{glob_regex}(/|$)");
- let regex = Regex::new(&pattern).ok()?;
-
- Some(ExcludePattern { regex, negate })
-}
-
-/// Apply a chain of exclude patterns to a relative path (as a `/`-prefixed string).
-///
-/// Patterns are applied in order; later patterns override earlier ones.
-/// Returns `true` if the file is excluded by the final matching pattern
-/// (or by `initially_excluded` if no pattern matches).
-fn apply_filters(
- path_with_slash: &str,
- patterns: &[ExcludePattern],
- initially_excluded: bool,
-) -> bool {
- let mut excluded = initially_excluded;
- for pat in patterns {
- if pat.regex.is_match(path_with_slash) {
- // A negate pattern re-includes; a normal pattern excludes
- excluded = !pat.negate;
- }
- }
- excluded
-}
-
-/// Parse `.gitattributes` from the source directory.
-///
-/// Returns exclude patterns for lines containing `export-ignore` or
-/// `-export-ignore`.
-pub fn parse_gitattributes(source_dir: &Path) -> Vec<ExcludePattern> {
- let path = source_dir.join(".gitattributes");
- let content = match fs::read_to_string(&path) {
- Ok(c) => c,
- Err(_) => return vec![],
- };
-
- let mut patterns = Vec::new();
- for line in content.lines() {
- let line = line.trim();
- if line.is_empty() || line.starts_with('#') {
- continue;
- }
- let parts: Vec<&str> = line.split_whitespace().collect();
- if parts.len() < 2 {
- continue;
- }
- let file_pattern = parts[0];
- // Check each attribute token for export-ignore / -export-ignore
- for attr in &parts[1..] {
- if *attr == "export-ignore" {
- if let Some(p) = parse_gitignore_pattern(file_pattern) {
- patterns.push(p);
- }
- } else if *attr == "-export-ignore" {
- // Negation: re-include files that would otherwise be excluded
- let negated = format!("!{}", file_pattern);
- if let Some(p) = parse_gitignore_pattern(&negated) {
- patterns.push(p);
- }
- }
- }
- }
- patterns
-}
-
-/// Convert `composer.json` `archive.exclude` rules into exclude patterns.
-pub fn parse_composer_excludes(excludes: &[String]) -> Vec<ExcludePattern> {
- excludes
- .iter()
- .filter_map(|rule| parse_gitignore_pattern(rule))
- .collect()
-}
-
-const VCS_DIRS: &[&str] = &[".git", ".svn", ".hg", "CVS", ".bzr"];
-
-/// Collect all archivable files from the source directory.
-///
-/// Returns paths relative to `source_dir`, sorted for deterministic output.
-/// Applies `exclude_patterns` to filter files. VCS directories are always
-/// skipped. Symlinks pointing outside `source_dir` are excluded.
-pub fn collect_archivable_files(
- source_dir: &Path,
- exclude_patterns: &[ExcludePattern],
-) -> anyhow::Result<Vec<PathBuf>> {
- let source_dir = source_dir
- .canonicalize()
- .unwrap_or_else(|_| source_dir.to_path_buf());
- let mut files = Vec::new();
- collect_recursive(&source_dir, &source_dir, exclude_patterns, &mut files)?;
- files.sort();
- Ok(files)
-}
-
-fn collect_recursive(
- source_dir: &Path,
- current_dir: &Path,
- exclude_patterns: &[ExcludePattern],
- out: &mut Vec<PathBuf>,
-) -> anyhow::Result<()> {
- let entries = fs::read_dir(current_dir)
- .with_context(|| format!("Failed to read directory: {}", current_dir.display()))?;
-
- let mut items: Vec<_> = entries.filter_map(|e| e.ok()).collect();
- // Sort for determinism
- items.sort_by_key(|e| e.file_name());
-
- for entry in items {
- let path = entry.path();
- let file_name = entry.file_name();
- let name_str = file_name.to_string_lossy();
-
- // Skip VCS directories
- if VCS_DIRS.contains(&name_str.as_ref()) {
- continue;
- }
-
- // Compute the relative path (forward-slash, prefixed with `/` for filter matching)
- let relative = path
- .strip_prefix(source_dir)
- .unwrap_or(&path)
- .to_string_lossy()
- .replace('\\', "/");
- let path_with_slash = format!("/{}", relative);
-
- // Check if this entry is excluded
- if apply_filters(&path_with_slash, exclude_patterns, false) {
- continue;
- }
-
- let metadata = match entry.metadata() {
- Ok(m) => m,
- Err(_) => continue,
- };
-
- if metadata.is_symlink() {
- // Resolve the symlink; skip if it points outside source_dir
- if let Ok(resolved) = fs::canonicalize(&path) {
- if !resolved.starts_with(source_dir) {
- continue;
- }
- out.push(PathBuf::from(&relative));
- }
- // If canonicalize fails, skip the symlink
- } else if metadata.is_dir() {
- // Collect children recursively
- let mut children = Vec::new();
- collect_recursive(source_dir, &path, exclude_patterns, &mut children)?;
- if children.is_empty() {
- // Include empty directory
- out.push(PathBuf::from(&relative));
- } else {
- out.extend(children);
- }
- } else {
- out.push(PathBuf::from(&relative));
- }
- }
-
- Ok(())
-}
-
-/// Supported archive formats.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum ArchiveFormat {
- Zip,
- Tar,
- TarGz,
- TarBz2,
-}
-
-impl ArchiveFormat {
- /// Parse a format string (case-insensitive). Returns `None` for unsupported formats.
- pub fn parse(s: &str) -> Option<Self> {
- match s.to_lowercase().as_str() {
- "zip" => Some(Self::Zip),
- "tar" => Some(Self::Tar),
- "tar.gz" | "tgz" => Some(Self::TarGz),
- "tar.bz2" => Some(Self::TarBz2),
- _ => None,
- }
- }
-
- /// File extension for this format.
- pub fn extension(&self) -> &str {
- match self {
- Self::Zip => "zip",
- Self::Tar => "tar",
- Self::TarGz => "tar.gz",
- Self::TarBz2 => "tar.bz2",
- }
- }
-}
-
-/// Create an archive of the given files.
-///
-/// - `source_dir`: the root of the source tree
-/// - `files`: relative paths (as returned by `collect_archivable_files`)
-/// - `target`: full output path including extension
-/// - `format`: the archive format to create
-pub fn create_archive(
- source_dir: &Path,
- files: &[PathBuf],
- target: &Path,
- format: &ArchiveFormat,
-) -> anyhow::Result<()> {
- match format {
- ArchiveFormat::Zip => create_zip(source_dir, files, target),
- ArchiveFormat::Tar => create_tar(source_dir, files, target),
- ArchiveFormat::TarGz => create_tar_gz(source_dir, files, target),
- ArchiveFormat::TarBz2 => create_tar_bz2(source_dir, files, target),
- }
-}
-
-fn create_zip(source_dir: &Path, files: &[PathBuf], target: &Path) -> anyhow::Result<()> {
- use zip::write::SimpleFileOptions;
-
- let file = fs::File::create(target)
- .with_context(|| format!("Failed to create archive: {}", target.display()))?;
- let mut writer = zip::ZipWriter::new(file);
-
- for rel in files {
- let abs = source_dir.join(rel);
- let rel_str = rel.to_string_lossy().replace('\\', "/");
-
- if abs.is_dir() {
- let opts = SimpleFileOptions::default();
- writer.add_directory(&rel_str, opts)?;
- } else {
- let metadata = fs::metadata(&abs)?;
-
- #[cfg(unix)]
- let opts = {
- use std::os::unix::fs::MetadataExt;
- let mode = metadata.mode();
- SimpleFileOptions::default()
- .compression_method(zip::CompressionMethod::Deflated)
- .unix_permissions(mode)
- };
-
- #[cfg(not(unix))]
- let opts =
- SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
-
- let _ = metadata; // suppress unused warning on non-unix
-
- writer.start_file(&rel_str, opts)?;
- let content = fs::read(&abs)?;
- writer.write_all(&content)?;
- }
- }
-
- writer.finish()?;
- Ok(())
-}
-
-fn create_tar(source_dir: &Path, files: &[PathBuf], target: &Path) -> anyhow::Result<()> {
- let file = fs::File::create(target)
- .with_context(|| format!("Failed to create archive: {}", target.display()))?;
- let mut builder = tar::Builder::new(file);
-
- for rel in files {
- let abs = source_dir.join(rel);
- if abs.is_dir() {
- builder.append_dir(rel, &abs)?;
- } else {
- builder.append_path_with_name(&abs, rel)?;
- }
- }
-
- builder.finish()?;
- Ok(())
-}
-
-fn create_tar_gz(source_dir: &Path, files: &[PathBuf], target: &Path) -> anyhow::Result<()> {
- let file = fs::File::create(target)
- .with_context(|| format!("Failed to create archive: {}", target.display()))?;
- let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
- let mut builder = tar::Builder::new(encoder);
-
- for rel in files {
- let abs = source_dir.join(rel);
- if abs.is_dir() {
- builder.append_dir(rel, &abs)?;
- } else {
- builder.append_path_with_name(&abs, rel)?;
- }
- }
-
- builder.into_inner()?.finish()?;
- Ok(())
-}
-
-fn create_tar_bz2(source_dir: &Path, files: &[PathBuf], target: &Path) -> anyhow::Result<()> {
- let file = fs::File::create(target)
- .with_context(|| format!("Failed to create archive: {}", target.display()))?;
- let encoder = bzip2::write::BzEncoder::new(file, bzip2::Compression::default());
- let mut builder = tar::Builder::new(encoder);
-
- for rel in files {
- let abs = source_dir.join(rel);
- if abs.is_dir() {
- builder.append_dir(rel, &abs)?;
- } else {
- builder.append_path_with_name(&abs, rel)?;
- }
- }
-
- builder.into_inner()?.finish()?;
- Ok(())
-}
-
-/// Generate an archive filename (without extension) for a package.
-///
-/// Mirrors Composer's `ArchiveManager::getPackageFilenameParts()`.
-pub fn generate_archive_filename(
- name: &str,
- archive_name: Option<&str>,
- version: Option<&str>,
- dist_reference: Option<&str>,
- dist_type: Option<&str>,
- source_reference: Option<&str>,
-) -> String {
- // Base: archive_name if set, otherwise replace non-alphanumeric chars with `-`
- let base = if let Some(an) = archive_name {
- an.to_string()
- } else {
- let re = Regex::new(r"[^a-zA-Z0-9_\-]").unwrap();
- re.replace_all(name, "-").to_string()
- };
-
- let mut parts: Vec<String> = vec![base];
-
- // Determine if dist_reference is a 40-char hex (SHA-1 commit hash)
- let is_sha_dist_ref = dist_reference
- .map(|r| r.len() == 40 && r.chars().all(|c| c.is_ascii_hexdigit()))
- .unwrap_or(false);
-
- if is_sha_dist_ref {
- // Append dist_reference and dist_type
- if let Some(dr) = dist_reference {
- parts.push(dr.to_string());
- }
- if let Some(dt) = dist_type {
- parts.push(dt.to_string());
- }
- } else {
- // Append version (if any), then dist_reference (if any)
- if let Some(v) = version {
- parts.push(v.to_string());
- }
- if let Some(dr) = dist_reference {
- parts.push(dr.to_string());
- }
- }
-
- // Append first 6 chars of SHA-1 of source_reference (if any)
- if let Some(sr) = source_reference {
- let mut hasher = Sha1::new();
- hasher.update(sr.as_bytes());
- let hash = format!("{:x}", hasher.finalize());
- parts.push(hash[..6.min(hash.len())].to_string());
- }
-
- // Replace `/` with `-` in each part, then join
- parts
- .iter()
- .map(|p| p.replace('/', "-"))
- .collect::<Vec<_>>()
- .join("-")
-}
-
-/// The set of archive extensions we support.
-const ARCHIVE_EXTENSIONS: &[&str] = &["zip", "tar", "tar.gz", "tar.bz2"];
-
-/// Generate patterns to exclude previous archives of this package from the archive.
-///
-/// If `has_extra_parts` is true (version/ref was appended), the pattern is
-/// `<base>-*.<ext>`. Otherwise it's `<base>.<ext>`.
-pub fn self_exclusion_patterns(base_name: &str, has_extra_parts: bool) -> Vec<String> {
- ARCHIVE_EXTENSIONS
- .iter()
- .map(|ext| {
- if has_extra_parts {
- format!("/{}-*.{}", base_name, ext)
- } else {
- format!("/{}.{}", base_name, ext)
- }
- })
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use tempfile::tempdir;
-
- // Note: glob_to_regex produces a *fragment* for use inside a larger pattern.
- // We test it by embedding it in a full anchored regex.
-
- fn full_pattern(glob: &str) -> Regex {
- // Simulate the unanchored pattern: `/fragment(/|$)`
- Regex::new(&format!("/{glob_re}(/|$)", glob_re = glob_to_regex(glob))).unwrap()
- }
-
- #[test]
- fn test_glob_to_regex_star() {
- let re = full_pattern("*.txt");
- // Unanchored pattern: matches any .txt file at any depth
- assert!(re.is_match("/foo.txt"));
- // Also matches nested .txt files (unanchored `/` prefix)
- assert!(re.is_match("/a/b.txt"));
- // Does NOT match non-.txt files
- assert!(!re.is_match("/foo.php"));
- }
-
- #[test]
- fn test_glob_to_regex_double_star() {
- // Double star matches across path separators
- let frag = glob_to_regex("**/*.txt");
- let re = Regex::new(&format!("/{frag}(/|$)")).unwrap();
- assert!(re.is_match("/a/b/c.txt"));
- }
-
- #[test]
- fn test_glob_to_regex_question() {
- let frag = glob_to_regex("?.txt");
- let re = Regex::new(&format!("/{frag}(/|$)")).unwrap();
- assert!(re.is_match("/a.txt"));
- assert!(!re.is_match("/ab.txt"));
- }
-
- #[test]
- fn test_glob_to_regex_bracket() {
- let frag = glob_to_regex("[abc].txt");
- let re = Regex::new(&format!("/{frag}(/|$)")).unwrap();
- assert!(re.is_match("/a.txt"));
- assert!(re.is_match("/b.txt"));
- assert!(!re.is_match("/d.txt"));
- }
-
- #[test]
- fn test_parse_gitignore_simple() {
- let pat = parse_gitignore_pattern("docs/").unwrap();
- assert!(!pat.negate);
- // "/docs" should match
- assert!(pat.regex.is_match("/docs"));
- }
-
- #[test]
- fn test_parse_gitignore_negated() {
- let pat = parse_gitignore_pattern("!important.txt").unwrap();
- assert!(pat.negate);
- }
-
- #[test]
- fn test_parse_gitignore_rooted() {
- let pat = parse_gitignore_pattern("/build").unwrap();
- assert!(!pat.negate);
- // Should match at root
- assert!(pat.regex.is_match("/build"));
- // Should NOT match in subdirectory (rooted pattern)
- assert!(!pat.regex.is_match("/src/build"));
- }
-
- #[test]
- fn test_parse_gitignore_unrooted() {
- let pat = parse_gitignore_pattern("*.log").unwrap();
- assert!(!pat.negate);
- // Should match anywhere
- assert!(pat.regex.is_match("/app.log"));
- assert!(pat.regex.is_match("/sub/dir/foo.log"));
- }
-
- #[test]
- fn test_parse_gitattributes_export_ignore() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join(".gitattributes"), "tests/ export-ignore\n").unwrap();
- let patterns = parse_gitattributes(dir.path());
- assert_eq!(patterns.len(), 1);
- assert!(!patterns[0].negate);
- assert!(patterns[0].regex.is_match("/tests"));
- }
-
- #[test]
- fn test_parse_gitattributes_neg_export_ignore() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join(".gitattributes"), "tests/ -export-ignore\n").unwrap();
- let patterns = parse_gitattributes(dir.path());
- assert_eq!(patterns.len(), 1);
- assert!(patterns[0].negate);
- }
-
- #[test]
- fn test_parse_gitattributes_comment() {
- let dir = tempdir().unwrap();
- fs::write(
- dir.path().join(".gitattributes"),
- "# comment\ntests/ export-ignore\n",
- )
- .unwrap();
- let patterns = parse_gitattributes(dir.path());
- assert_eq!(patterns.len(), 1);
- }
-
- #[test]
- fn test_parse_gitattributes_non_export() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join(".gitattributes"), "*.php text\n").unwrap();
- let patterns = parse_gitattributes(dir.path());
- assert!(patterns.is_empty());
- }
-
- #[test]
- fn test_parse_gitattributes_missing_file() {
- let dir = tempdir().unwrap();
- let patterns = parse_gitattributes(dir.path());
- assert!(patterns.is_empty());
- }
-
- #[test]
- fn test_collect_files_basic() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join("a.php"), b"<?php").unwrap();
- fs::write(dir.path().join("b.php"), b"<?php").unwrap();
- fs::create_dir(dir.path().join("src")).unwrap();
- fs::write(dir.path().join("src").join("c.php"), b"<?php").unwrap();
-
- let files = collect_archivable_files(dir.path(), &[]).unwrap();
- let strs: Vec<String> = files
- .iter()
- .map(|p| p.to_string_lossy().to_string())
- .collect();
- assert!(strs.contains(&"a.php".to_string()));
- assert!(strs.contains(&"b.php".to_string()));
- assert!(strs.contains(&"src/c.php".to_string()));
- }
-
- #[test]
- fn test_collect_files_excludes() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join("main.php"), b"<?php").unwrap();
- fs::create_dir(dir.path().join("tests")).unwrap();
- fs::write(dir.path().join("tests").join("test.php"), b"<?php").unwrap();
-
- let patterns = vec![parse_gitignore_pattern("tests/").unwrap()];
- let files = collect_archivable_files(dir.path(), &patterns).unwrap();
- let strs: Vec<String> = files
- .iter()
- .map(|p| p.to_string_lossy().to_string())
- .collect();
- assert!(strs.contains(&"main.php".to_string()));
- assert!(!strs.iter().any(|s| s.starts_with("tests")));
- }
-
- #[test]
- fn test_collect_files_skips_vcs() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join("main.php"), b"<?php").unwrap();
- fs::create_dir(dir.path().join(".git")).unwrap();
- fs::write(
- dir.path().join(".git").join("HEAD"),
- b"ref: refs/heads/main",
- )
- .unwrap();
-
- let files = collect_archivable_files(dir.path(), &[]).unwrap();
- let strs: Vec<String> = files
- .iter()
- .map(|p| p.to_string_lossy().to_string())
- .collect();
- assert!(strs.contains(&"main.php".to_string()));
- assert!(!strs.iter().any(|s| s.starts_with(".git")));
- }
-
- #[test]
- fn test_collect_files_empty_dir() {
- let dir = tempdir().unwrap();
- fs::write(dir.path().join("main.php"), b"<?php").unwrap();
- fs::create_dir(dir.path().join("empty_dir")).unwrap();
-
- let files = collect_archivable_files(dir.path(), &[]).unwrap();
- let strs: Vec<String> = files
- .iter()
- .map(|p| p.to_string_lossy().to_string())
- .collect();
- assert!(strs.contains(&"main.php".to_string()));
- assert!(strs.contains(&"empty_dir".to_string()));
- }
-
- fn make_source_tree(dir: &Path) {
- fs::write(dir.join("main.php"), b"<?php echo 'hello';").unwrap();
- fs::create_dir(dir.join("src")).unwrap();
- fs::write(dir.join("src").join("Foo.php"), b"<?php class Foo {}").unwrap();
- }
-
- #[test]
- fn test_create_zip_archive() {
- let src = tempdir().unwrap();
- make_source_tree(src.path());
- let out = tempdir().unwrap();
- let target = out.path().join("test.zip");
-
- let files = collect_archivable_files(src.path(), &[]).unwrap();
- create_archive(src.path(), &files, &target, &ArchiveFormat::Zip).unwrap();
- assert!(target.exists());
-
- // Verify contents
- let zip_data = fs::read(&target).unwrap();
- let cursor = std::io::Cursor::new(zip_data);
- let mut archive = zip::ZipArchive::new(cursor).unwrap();
- let names: Vec<String> = (0..archive.len())
- .map(|i| archive.by_index(i).unwrap().name().to_string())
- .collect();
- assert!(names.contains(&"main.php".to_string()));
- assert!(names.contains(&"src/Foo.php".to_string()));
- }
-
- #[test]
- fn test_create_tar_archive() {
- let src = tempdir().unwrap();
- make_source_tree(src.path());
- let out = tempdir().unwrap();
- let target = out.path().join("test.tar");
-
- let files = collect_archivable_files(src.path(), &[]).unwrap();
- create_archive(src.path(), &files, &target, &ArchiveFormat::Tar).unwrap();
- assert!(target.exists());
-
- // Verify contents
- let tar_data = fs::read(&target).unwrap();
- let cursor = std::io::Cursor::new(tar_data);
- let mut archive = tar::Archive::new(cursor);
- let names: Vec<String> = archive
- .entries()
- .unwrap()
- .filter_map(|e| e.ok())
- .filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().to_string()))
- .collect();
- assert!(names.contains(&"main.php".to_string()));
- assert!(names.contains(&"src/Foo.php".to_string()));
- }
-
- #[test]
- fn test_create_tar_gz_archive() {
- let src = tempdir().unwrap();
- make_source_tree(src.path());
- let out = tempdir().unwrap();
- let target = out.path().join("test.tar.gz");
-
- let files = collect_archivable_files(src.path(), &[]).unwrap();
- create_archive(src.path(), &files, &target, &ArchiveFormat::TarGz).unwrap();
- assert!(target.exists());
-
- let gz_data = fs::read(&target).unwrap();
- let cursor = std::io::Cursor::new(gz_data);
- let decoder = flate2::read::GzDecoder::new(cursor);
- let mut archive = tar::Archive::new(decoder);
- let names: Vec<String> = archive
- .entries()
- .unwrap()
- .filter_map(|e| e.ok())
- .filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().to_string()))
- .collect();
- assert!(names.contains(&"main.php".to_string()));
- }
-
- #[test]
- fn test_create_tar_bz2_archive() {
- let src = tempdir().unwrap();
- make_source_tree(src.path());
- let out = tempdir().unwrap();
- let target = out.path().join("test.tar.bz2");
-
- let files = collect_archivable_files(src.path(), &[]).unwrap();
- create_archive(src.path(), &files, &target, &ArchiveFormat::TarBz2).unwrap();
- assert!(target.exists());
-
- let bz_data = fs::read(&target).unwrap();
- let cursor = std::io::Cursor::new(bz_data);
- let decoder = bzip2::read::BzDecoder::new(cursor);
- let mut archive = tar::Archive::new(decoder);
- let names: Vec<String> = archive
- .entries()
- .unwrap()
- .filter_map(|e| e.ok())
- .filter_map(|e| e.path().ok().map(|p| p.to_string_lossy().to_string()))
- .collect();
- assert!(names.contains(&"main.php".to_string()));
- }
-
- #[cfg(unix)]
- #[test]
- fn test_zip_preserves_permissions() {
- use std::os::unix::fs::PermissionsExt;
-
- let src = tempdir().unwrap();
- let script = src.path().join("run.sh");
- fs::write(&script, b"#!/bin/sh\necho hello").unwrap();
- fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
-
- let out = tempdir().unwrap();
- let target = out.path().join("test.zip");
- let files = collect_archivable_files(src.path(), &[]).unwrap();
- create_archive(src.path(), &files, &target, &ArchiveFormat::Zip).unwrap();
-
- let zip_data = fs::read(&target).unwrap();
- let cursor = std::io::Cursor::new(zip_data);
- let mut archive = zip::ZipArchive::new(cursor).unwrap();
- let entry = archive.by_name("run.sh").unwrap();
- let mode = entry.unix_mode().unwrap_or(0);
- // Lower 9 bits should be 0o755
- assert_eq!(mode & 0o777, 0o755);
- }
-
- #[test]
- fn test_filename_simple_package() {
- let name = generate_archive_filename("vendor/pkg", None, Some("1.2.3"), None, None, None);
- assert_eq!(name, "vendor-pkg-1.2.3");
- }
-
- #[test]
- fn test_filename_with_archive_name() {
- let name = generate_archive_filename(
- "vendor/pkg",
- Some("my-package"),
- Some("1.0.0"),
- None,
- None,
- None,
- );
- assert_eq!(name, "my-package-1.0.0");
- }
-
- #[test]
- fn test_filename_with_sha_dist_ref() {
- let sha = "a".repeat(40);
- let name = generate_archive_filename(
- "vendor/pkg",
- None,
- Some("1.0.0"),
- Some(&sha),
- Some("zip"),
- None,
- );
- // 40-char hex → append dist_ref and dist_type, not version
- assert_eq!(name, format!("vendor-pkg-{}-zip", sha));
- }
-
- #[test]
- fn test_filename_with_source_ref() {
- let name = generate_archive_filename(
- "vendor/pkg",
- None,
- Some("1.0.0"),
- None,
- None,
- Some("abc123"),
- );
- // Appends first 6 chars of SHA-1 of "abc123"
- let mut hasher = Sha1::new();
- hasher.update(b"abc123");
- let hash = format!("{:x}", hasher.finalize());
- let expected = format!("vendor-pkg-1.0.0-{}", &hash[..6]);
- assert_eq!(name, expected);
- }
-
- #[test]
- fn test_filename_slashes_replaced() {
- let name =
- generate_archive_filename("vendor/my-pkg", None, Some("1.0/beta"), None, None, None);
- assert_eq!(name, "vendor-my-pkg-1.0-beta");
- }
-
- #[test]
- fn test_self_exclusion_patterns_with_extra_parts() {
- let patterns = self_exclusion_patterns("vendor-pkg", true);
- assert!(patterns.contains(&"/vendor-pkg-*.zip".to_string()));
- assert!(patterns.contains(&"/vendor-pkg-*.tar".to_string()));
- assert!(patterns.contains(&"/vendor-pkg-*.tar.gz".to_string()));
- assert!(patterns.contains(&"/vendor-pkg-*.tar.bz2".to_string()));
- }
-
- #[test]
- fn test_self_exclusion_patterns_no_extra_parts() {
- let patterns = self_exclusion_patterns("vendor-pkg", false);
- assert!(patterns.contains(&"/vendor-pkg.zip".to_string()));
- assert!(patterns.contains(&"/vendor-pkg.tar".to_string()));
- }
-}