extern crate fancy_regex;
extern crate flate2;
extern crate lazy_static;
extern crate normalize_path;
extern crate serde;
extern crate tar;
extern crate toml;
extern crate walkdir;
use self::serde::{Deserialize, Serialize};
use normalize_path::NormalizePath;
use std::env;
use std::fs;
use std::io;
use std::path;
use std::time;
lazy_static::lazy_static! {
pub static ref EXTENSIONED_FILE_PATH_PATTERN: fancy_regex::Regex = fancy_regex::Regex::new(r"^(.*/)*[^/]*\.[^/]*$").unwrap();
pub static ref FILE_MANAGER_CACHE_PATTERN: fancy_regex::Regex = fancy_regex::Regex::new(r"^(.*/)?(\.DS_Store|Thumbs\.db)$").unwrap();
pub static ref SYSTEM_V_INIT_LINEAGE_PATTERN: fancy_regex::Regex = fancy_regex::Regex::new(r"^(.*/)?etc/init\.d(/.*)?$").unwrap();
pub static ref COMMON_NONEXECUTABLE_FILE_PATH_PATTERN: fancy_regex::Regex = fancy_regex::Regex::new(r"(?i)^aliases|(ba|(m)?k|z)shrc|(bsd|gnu)?makefile|changelog|exports|fstab|license|readme|group|hosts|issue|mime|modules|profile|protocols|resolv|services|t(e)?mp|zshenv|((.*/)?etc/.+)$").unwrap();
}
#[test]
fn test_extensioned_file_path_pattern() -> Result<(), fancy_regex::Error> {
let pattern = EXTENSIONED_FILE_PATH_PATTERN.clone();
assert!(!pattern.is_match("hello")?);
assert!(!pattern.is_match("HELLO")?);
assert!(!pattern.is_match("hello-1.0/docs")?);
assert!(pattern.is_match("HELLO.BAT")?);
assert!(pattern.is_match("hello.bat")?);
assert!(pattern.is_match("applications/hello.bat")?);
assert!(pattern.is_match("HELLO.EXE")?);
assert!(pattern.is_match("hello.exe")?);
assert!(pattern.is_match("applications/hello.exe")?);
assert!(pattern.is_match(".gitignore")?);
assert!(pattern.is_match("DEGENERATE.")?);
assert!(pattern.is_match("degenerate.")?);
Ok(())
}
#[test]
fn test_file_manager_cache_pattern() -> Result<(), fancy_regex::Error> {
let pattern = FILE_MANAGER_CACHE_PATTERN.clone();
assert!(pattern.is_match(".DS_Store")?);
assert!(pattern.is_match("docs/.DS_Store")?);
assert!(pattern.is_match("/docs/.DS_Store")?);
assert!(!pattern.is_match("docs")?);
assert!(!pattern.is_match("/docs")?);
assert!(pattern.is_match("Thumbs.db")?);
assert!(pattern.is_match("docs/Thumbs.db")?);
Ok(())
}
#[test]
fn test_system_v_init_lineage_pattern() -> Result<(), fancy_regex::Error> {
let pattern = SYSTEM_V_INIT_LINEAGE_PATTERN.clone();
assert!(pattern.is_match("/etc/init.d")?);
assert!(pattern.is_match("etc/init.d")?);
assert!(pattern.is_match("/etc/init.d/ssh")?);
assert!(pattern.is_match("etc/init.d/ssh")?);
assert!(!pattern.is_match("/root/.ssh")?);
assert!(!pattern.is_match("root/.ssh")?);
Ok(())
}
#[test]
fn test_common_nonexecutable_file_path_pattern() -> Result<(), fancy_regex::Error> {
let pattern = COMMON_NONEXECUTABLE_FILE_PATH_PATTERN.clone();
assert!(pattern.is_match("bashrc")?);
assert!(pattern.is_match("bsdmakefile")?);
assert!(pattern.is_match("changelog")?);
assert!(pattern.is_match("gnumakefile")?);
assert!(pattern.is_match("license")?);
assert!(pattern.is_match("makefile")?);
assert!(pattern.is_match("README")?);
assert!(pattern.is_match("readme")?);
assert!(pattern.is_match("aliases")?);
assert!(pattern.is_match("exports")?);
assert!(pattern.is_match("fstab")?);
assert!(pattern.is_match("group")?);
assert!(pattern.is_match("hosts")?);
assert!(pattern.is_match("issue")?);
assert!(pattern.is_match("kshrc")?);
assert!(pattern.is_match("mime")?);
assert!(pattern.is_match("mkshrc")?);
assert!(pattern.is_match("modules")?);
assert!(pattern.is_match("profile")?);
assert!(pattern.is_match("protocols")?);
assert!(pattern.is_match("resolv")?);
assert!(pattern.is_match("services")?);
assert!(pattern.is_match("temp")?);
assert!(pattern.is_match("tmp")?);
assert!(pattern.is_match("zshenv")?);
assert!(pattern.is_match("zshrc")?);
assert!(pattern.is_match("/etc/sshd/sshd_config")?);
assert!(pattern.is_match("etc/sshd/sshd_config")?);
Ok(())
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub enum HeaderType {
Old,
Gnu,
UStar,
}
#[derive(Debug, PartialEq, Deserialize, Serialize)]
pub enum FileMode {
Directory,
File,
}
#[derive(Debug)]
pub struct Condition {
pub mode: Option<FileMode>,
pub path: Option<fancy_regex::Regex>,
}
#[derive(Debug)]
pub struct Rule {
pub when: Condition,
pub skip: bool,
pub mtime: Option<u64>,
pub uid: Option<u64>,
pub gid: Option<u64>,
pub username: Option<String>,
pub groupname: Option<String>,
pub permissions: Option<u32>,
}
impl Rule {
pub fn is_match(&self, filemode: &FileMode, pth: &str) -> Result<bool, io::Error> {
if let Some(when_mode) = &self.when.mode
&& when_mode != filemode
{
return Ok(false);
}
if let Some(when_path) = &self.when.path
&& !when_path.is_match(pth).map_err(io::Error::other)?
{
return Ok(false);
}
Ok(true)
}
pub fn is_skip(&self, filemode: &FileMode, pth: &str) -> Result<bool, io::Error> {
self.is_match(filemode, pth).map(|e| e && self.skip)
}
pub fn apply(&self, header: &mut tar::Header) -> Result<(), io::Error> {
if let Some(mtime) = self.mtime {
header.set_mtime(mtime);
}
if let Some(uid) = self.uid {
header.set_uid(uid);
}
if let Some(gid) = self.gid {
header.set_gid(gid);
}
if let Some(username) = &self.username {
header.set_username(username)?;
}
if let Some(groupname) = &self.groupname {
header.set_groupname(groupname)?;
}
if let Some(permissions) = &self.permissions {
header.set_mode(*permissions);
}
Ok(())
}
}
#[derive(Debug)]
pub struct Chandler {
pub verbose: bool,
pub header_type: HeaderType,
pub cwd: Option<path::PathBuf>,
pub rules: Vec<Rule>,
}
pub fn permissions_to_u32(permissions: fs::Permissions) -> u32 {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
permissions.mode()
}
#[cfg(windows)]
{
if permissions.readonly() {
0o444u32
} else {
0o666u32
}
}
}
impl Default for Chandler {
fn default() -> Self {
Chandler {
verbose: false,
header_type: HeaderType::UStar,
cwd: None,
rules: vec![
Rule {
when: Condition {
mode: None,
path: Some(FILE_MANAGER_CACHE_PATTERN.clone()),
},
skip: true,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: None,
},
Rule {
when: Condition {
mode: None,
path: None,
},
skip: false,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: Some(0o755u32),
},
Rule {
when: Condition {
mode: Some(FileMode::File),
path: Some(COMMON_NONEXECUTABLE_FILE_PATH_PATTERN.clone()),
},
skip: false,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: Some(0o644u32),
},
Rule {
when: Condition {
mode: Some(FileMode::File),
path: Some(EXTENSIONED_FILE_PATH_PATTERN.clone()),
},
skip: false,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: Some(0o644u32),
},
Rule {
when: Condition {
mode: None,
path: Some(SYSTEM_V_INIT_LINEAGE_PATTERN.clone()),
},
skip: false,
mtime: None,
uid: None,
gid: None,
username: None,
groupname: None,
permissions: Some(0o755u32),
},
],
}
}
}
impl Chandler {
pub fn archive(&self, target: &path::Path, source: &path::Path) -> Result<(), io::Error> {
if let Some(cwd_pathbuf) = &self.cwd {
env::set_current_dir(cwd_pathbuf.as_path())?;
}
let file = fs::File::create(target)?;
let gz_encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(gz_encoder);
let walker = walkdir::WalkDir::new(source).sort_by(
|a: &walkdir::DirEntry, b: &walkdir::DirEntry| a.file_name().cmp(b.file_name()),
);
for entry in walker {
let entry = entry?;
let pth = entry.path();
let pth_clean = pth.normalize();
let pth_str = pth_clean.to_str().ok_or_else(|| {
io::Error::other(format!("unable to render path {:?}", pth_clean))
})?;
if pth_str.is_empty() || pth_str == "." {
continue;
}
let metadata = entry.metadata()?;
let mut header = match self.header_type {
HeaderType::Old => tar::Header::new_old(),
HeaderType::Gnu => tar::Header::new_gnu(),
HeaderType::UStar => tar::Header::new_ustar(),
};
header.set_path(&pth_clean)?;
let mtime = metadata
.modified()?
.duration_since(time::UNIX_EPOCH)
.map(|e| e.as_secs())
.map_err(io::Error::other)?;
header.set_mtime(mtime);
header.set_mode(permissions_to_u32(metadata.permissions()));
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
header.set_uid(metadata.uid() as u64);
header.set_gid(metadata.gid() as u64);
}
#[cfg(not(unix))]
{
eprintln!("warning: nonunix environment. dropping uid, gid.");
}
let filemode = if metadata.is_dir() {
FileMode::Directory
} else if metadata.is_file() {
FileMode::File
} else {
return Err(io::Error::other(format!(
"unsupported file type: {pth_str}"
)));
};
if filemode == FileMode::Directory {
header.set_entry_type(tar::EntryType::Directory);
header.set_size(0);
} else if filemode == FileMode::File {
header.set_size(metadata.len());
}
if self.verbose {
eprintln!("a {pth_str}");
}
if self
.rules
.iter()
.any(|e| e.is_skip(&filemode, pth_str).unwrap_or(false))
{
continue;
}
for rule in &self.rules {
if !rule.is_match(&filemode, pth_str)? {
continue;
}
rule.apply(&mut header)?;
}
header.set_cksum();
if filemode == FileMode::Directory {
builder.append(&header, &[] as &[u8])?;
} else if filemode == FileMode::File {
let mut source_file = fs::File::open(pth_clean)?;
builder.append(&header, &mut source_file)?;
}
}
builder.into_inner()?.finish().map(|_| ())
}
}