use crate::parse::{Token, WordSet};
use crate::verdict::{SafetyLevel, Verdict};
static TAR_DANGEROUS_LONG: WordSet = WordSet::new(&[
"--append", "--concatenate", "--create", "--delete",
"--extract", "--get", "--update",
]);
const TAR_DANGEROUS_SHORT: &[u8] = b"Acrux";
const TAR_SAFE_SHORT: &[u8] = b"JfjtvzO";
static TAR_SAFE_LONG: WordSet = WordSet::new(&[
"--bzip2", "--file", "--gzip", "--list", "--verbose", "--xz", "--zstd",
]);
fn is_old_style_flags(s: &str) -> bool {
!s.starts_with('-') && !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphabetic())
}
fn has_list_mode(tokens: &[Token]) -> bool {
for (idx, t) in tokens[1..].iter().enumerate() {
if *t == "--list" {
return true;
}
let s = t.as_str();
if s.starts_with('-') && !s.starts_with("--")
&& s.bytes().skip(1).any(|b| b == b't')
{
return true;
}
if idx == 0 && is_old_style_flags(s) && s.contains('t') {
return true;
}
}
false
}
fn has_dangerous_char(s: &str) -> bool {
s.bytes().skip(1).any(|b| TAR_DANGEROUS_SHORT.contains(&b))
}
fn all_chars_safe(s: &str) -> bool {
s.bytes().skip(1).all(|b| TAR_SAFE_SHORT.contains(&b) || b == b't')
}
fn check_short_bundle(s: &str) -> Option<usize> {
if has_dangerous_char(s) {
return None;
}
if !all_chars_safe(s) {
if s.contains('f') {
return Some(2);
}
return None;
}
if s.contains('f') { Some(2) } else { Some(1) }
}
fn is_safe_tar(tokens: &[Token]) -> Verdict {
if tokens.len() == 2 && (tokens[1] == "--help" || tokens[1] == "-h" || tokens[1] == "--version") {
return Verdict::Allowed(SafetyLevel::Inert);
}
if !has_list_mode(tokens) {
return Verdict::Denied;
}
let mut i = 1;
while i < tokens.len() {
let t = &tokens[i];
let s = t.as_str();
if TAR_DANGEROUS_LONG.contains(t) {
return Verdict::Denied;
}
if TAR_SAFE_LONG.contains(t) {
i += 1;
continue;
}
if s == "--file" || s == "-f" {
i += 2;
continue;
}
if s.starts_with('-') && !s.starts_with("--") && s.len() > 1 {
match check_short_bundle(s) {
Some(advance) => { i += advance; continue; }
None => return Verdict::Denied,
}
}
if i == 1 && is_old_style_flags(s) {
let dashed = format!("-{s}");
match check_short_bundle(&dashed) {
Some(advance) => { i += advance; continue; }
None => return Verdict::Denied,
}
}
if s.starts_with("--") {
return Verdict::Denied;
}
i += 1;
}
Verdict::Allowed(SafetyLevel::Inert)
}
pub(in crate::handlers::coreutils) fn dispatch(cmd: &str, tokens: &[Token]) -> Option<Verdict> {
match cmd {
"tar" => Some(is_safe_tar(tokens)),
_ => None,
}
}
pub(in crate::handlers::coreutils) fn command_docs() -> Vec<crate::docs::CommandDoc> {
vec![
crate::docs::CommandDoc::handler("tar",
"https://man7.org/linux/man-pages/man1/tar.1.html",
"Listing mode only (requires -t or --list). Old-style flags accepted (e.g. tar tf, tar tzf).\n\
Flags: -f, -j, -J, -v, -z, -O, --bzip2, --file, --gzip, --xz, --zstd."),
]
}
#[cfg(test)]
pub(in crate::handlers::coreutils) const REGISTRY: &[crate::handlers::CommandEntry] = &[
crate::handlers::CommandEntry::Custom { cmd: "tar", valid_prefix: Some("tar -tf archive.tar") },
];
#[cfg(test)]
mod tests {
use crate::is_safe_command;
fn check(cmd: &str) -> bool { is_safe_command(cmd) }
safe! {
tar_list: "tar -tf archive.tar",
tar_list_verbose: "tar -tvf archive.tar",
tar_list_gz: "tar -tzf archive.tar.gz",
tar_list_long: "tar --list --file archive.tar",
tar_list_bz2: "tar -tjf archive.tar.bz2",
tar_list_xz: "tar -tJf archive.tar.xz",
tar_list_separate: "tar -t -f archive.tar",
tar_list_v_separate: "tar -t -v -f archive.tar",
tar_old_style_tz: "tar tz",
tar_old_style_tf: "tar tf archive.tar",
tar_old_style_tvf: "tar tvf archive.tar",
tar_old_style_tzf: "tar tzf archive.tar.gz",
tar_old_style_tjf: "tar tjf archive.tar.bz2",
}
denied! {
tar_create: "tar -cf archive.tar files/",
tar_extract: "tar -xf archive.tar",
tar_append: "tar -rf archive.tar newfile",
tar_update: "tar -uf archive.tar newfile",
tar_bare: "tar",
tar_no_list: "tar -f archive.tar",
tar_bundled_extract: "tar -txf archive.tar",
tar_bundled_create: "tar -tcf archive.tar",
tar_old_style_xf: "tar xf archive.tar",
tar_old_style_cf: "tar cf archive.tar files/",
}
}