use std::path::Path;
use flate2::write::GzEncoder;
use flate2::Compression;
use crate::error::{ComposeError, Result};
pub(super) fn build_sync_tar(src: &Path, entry_name: &Path) -> Result<Vec<u8>> {
let encoder = GzEncoder::new(Vec::new(), Compression::default());
let mut tar = tar::Builder::new(encoder);
tar.follow_symlinks(false);
if src.is_dir() {
for abs in super::super::walk_dir(src).map_err(ComposeError::Io)? {
let rel = abs
.strip_prefix(src)
.map_err(|_| ComposeError::Build("path strip".into()))?;
let name = entry_name.join(rel);
let is_dir = abs.symlink_metadata().map(|m| m.is_dir()).unwrap_or(false);
if is_dir {
tar.append_dir(&name, &abs)
.map_err(|e| ComposeError::Build(e.to_string()))?;
} else {
tar.append_path_with_name(&abs, &name)
.map_err(|e| ComposeError::Build(e.to_string()))?;
}
}
} else {
tar.append_path_with_name(src, entry_name)
.map_err(|e| ComposeError::Build(e.to_string()))?;
}
let gz = tar
.into_inner()
.map_err(|e| ComposeError::Build(e.to_string()))?;
let bytes = gz
.finish()
.map_err(|e| ComposeError::Build(e.to_string()))?;
Ok(bytes)
}
pub(super) fn is_ignored(path: &str, patterns: &[String]) -> bool {
for pat in patterns {
if pat.ends_with('/') {
if path.starts_with(pat.as_str()) {
return true;
}
} else if path == pat.as_str()
|| (path.starts_with(pat.as_str()) && path.as_bytes().get(pat.len()) == Some(&b'/'))
{
return true;
}
}
false
}
pub(super) fn is_included(path: &str, patterns: &[String]) -> bool {
for pat in patterns {
if pat.starts_with("*.") {
let ext = &pat[1..];
if path.ends_with(ext) {
return true;
}
} else if pat.ends_with('/') {
if path.starts_with(pat.as_str()) {
return true;
}
} else if path == pat.as_str()
|| (path.len() > pat.len() + 1
&& path.as_bytes()[path.len() - pat.len() - 1] == b'/'
&& path.ends_with(pat.as_str()))
{
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use super::{build_sync_tar, is_ignored, is_included};
use flate2::read::GzDecoder;
use std::fs;
use std::io::Read;
use std::path::Path;
use tempfile::tempdir;
fn pats(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn tar_entry_paths(gz: &[u8]) -> Vec<String> {
let mut decoder = GzDecoder::new(gz);
let mut raw = Vec::new();
decoder.read_to_end(&mut raw).unwrap();
let mut archive = tar::Archive::new(&raw[..]);
let mut names = Vec::new();
for entry in archive.entries().unwrap() {
let entry = entry.unwrap();
if entry.header().entry_type().is_file() {
names.push(entry.path().unwrap().to_string_lossy().replace('\\', "/"));
}
}
names
}
#[test]
fn ignored_exact_file() {
assert!(is_ignored("Makefile", &pats(&["Makefile"])));
}
#[test]
fn ignored_not_prefix_match() {
assert!(!is_ignored("Makefile.local", &pats(&["Makefile"])));
}
#[test]
fn ignored_dir_prefix() {
assert!(is_ignored("node_modules/foo.js", &pats(&["node_modules/"])));
}
#[test]
fn ignored_dir_prefix_no_partial() {
assert!(!is_ignored("nonode_modules/foo", &pats(&["node_modules/"])));
}
#[test]
fn ignored_path_with_slash() {
assert!(is_ignored("vendor/lib.rs", &pats(&["vendor"])));
}
#[test]
fn ignored_empty_patterns() {
assert!(!is_ignored("anything.rs", &[]));
}
#[test]
fn ignored_no_match() {
assert!(!is_ignored("src/main.rs", &pats(&["target/", "*.log"])));
}
#[test]
fn included_glob_extension() {
assert!(is_included("src/main.rs", &pats(&["*.rs"])));
}
#[test]
fn included_glob_no_match() {
assert!(!is_included("src/main.go", &pats(&["*.rs"])));
}
#[test]
fn included_dir_prefix() {
assert!(is_included("src/main.rs", &pats(&["src/"])));
}
#[test]
fn included_dir_prefix_no_match() {
assert!(!is_included("test/main.rs", &pats(&["src/"])));
}
#[test]
fn included_exact_match() {
assert!(is_included("Makefile", &pats(&["Makefile"])));
}
#[test]
fn included_path_segment_suffix() {
assert!(is_included("src/lib.rs", &pats(&["lib.rs"])));
}
#[test]
fn included_empty_patterns() {
assert!(!is_included("anything", &[]));
}
#[test]
fn sync_tar_single_file() {
let dir = tempdir().unwrap();
let file = dir.path().join("hello.txt");
fs::write(&file, b"hello world").unwrap();
let bytes = build_sync_tar(&file, Path::new("hello.txt")).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
assert_eq!(tar_entry_paths(&bytes), vec!["hello.txt"]);
}
#[test]
fn sync_tar_single_file_renamed_entry() {
let dir = tempdir().unwrap();
let file = dir.path().join("settings.yml");
fs::write(&file, b"k: v").unwrap();
let bytes = build_sync_tar(&file, Path::new("config.yml")).unwrap();
assert_eq!(tar_entry_paths(&bytes), vec!["config.yml"]);
}
#[test]
fn sync_tar_subpath_entry_preserved() {
let dir = tempdir().unwrap();
let file = dir.path().join("b.txt");
fs::write(&file, b"file b").unwrap();
let bytes = build_sync_tar(&file, Path::new("sub/b.txt")).unwrap();
assert_eq!(tar_entry_paths(&bytes), vec!["sub/b.txt"]);
}
#[test]
fn sync_tar_directory() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.txt"), b"file a").unwrap();
fs::create_dir(dir.path().join("sub")).unwrap();
fs::write(dir.path().join("sub/b.txt"), b"file b").unwrap();
let bytes = build_sync_tar(dir.path(), Path::new("dst")).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
let mut names = tar_entry_paths(&bytes);
names.sort();
assert_eq!(names, vec!["dst/a.txt", "dst/sub/b.txt"]);
}
#[test]
fn sync_tar_path_with_no_file_name() {
let dir = tempdir().unwrap();
let bytes = build_sync_tar(dir.path(), Path::new(".")).unwrap();
assert_eq!(&bytes[..2], &[0x1f, 0x8b]);
}
}