pub(super) use crate::compat::host::printf;
pub(super) const PROGRAM: &[u8] = b"gzip";
pub(super) const HELP: &str = concat!(
"Usage: %s [OPTION]... [FILE]...\n",
"Compress or uncompress FILEs (by default, compress FILES in-place).\n",
"\n",
"Mandatory arguments to long options are mandatory for short options too.\n",
"\n",
" -c, --stdout write on standard output, keep original files unchanged\n",
" -d, --decompress decompress\n",
" -f, --force force overwrite of output file and compress links\n",
" -h, --help give this help\n",
" -k, --keep keep (don't delete) input files\n",
" -l, --list list compressed file contents\n",
" -L, --license display software license\n",
" -n, --no-name do not save or restore the original name and timestamp\n",
" -N, --name save or restore the original name and timestamp\n",
" -q, --quiet suppress all warnings\n",
" -r, --recursive operate recursively on directories\n",
" --rsyncable make rsync-friendly archive\n",
" -S, --suffix=SUF use suffix SUF on compressed files\n",
" --synchronous synchronous output (safer if system crashes, but slower)\n",
" -t, --test test compressed file integrity\n",
" -v, --verbose verbose mode\n",
" -V, --version display version number\n",
" -1, --fast compress faster\n",
" -9, --best compress better\n",
"\n",
"With no FILE, or when FILE is -, read standard input.\n",
"\n",
"Report bugs to <bug-gzip@gnu.org>.\n",
);
pub(super) const VERSION_FIRST_LINE: &str =
concat!("gzip (stuffr ", env!("CARGO_PKG_VERSION"), ") 1.15\n");
pub(super) const LICENSE_REST: &str = concat!(
"Copyright (c) 2026 Thomas Bjork\n",
"License MIT: <https://opensource.org/license/mit>.\n",
"This is stuffr emulating GNU gzip's command-line interface; it contains no GNU code.\n",
"It is provided as is, without warranty of any kind.\n",
);
pub(super) const WRITTEN_BY: &str = "\nWritten by Thomas Bjork, for stuffr.\n";
pub(super) const GUNZIP_HELP: &str = concat!(
"Usage: %s [OPTION]... [FILE]...\n",
"Uncompress FILEs (by default, in-place).\n",
"\n",
"Mandatory arguments to long options are mandatory for short options too.\n",
"\n",
" -c, --stdout write on standard output, keep original files unchanged\n",
" -f, --force force overwrite of output file and compress links\n",
" -k, --keep keep (don't delete) input files\n",
" -l, --list list compressed file contents\n",
" -n, --no-name do not save or restore the original name and timestamp\n",
" -N, --name save or restore the original name and timestamp\n",
" -q, --quiet suppress all warnings\n",
" -r, --recursive operate recursively on directories\n",
" -S, --suffix=SUF use suffix SUF on compressed files\n",
" --synchronous synchronous output (safer if system crashes, but slower)\n",
" -t, --test test compressed file integrity\n",
" -v, --verbose verbose mode\n",
" --help display this help and exit\n",
" --version display version information and exit\n",
"\n",
"With no FILE, or when FILE is -, read standard input.\n",
"\n",
"Report bugs to <bug-gzip@gnu.org>.\n",
);
pub(super) const ZCAT_HELP: &str = concat!(
"Usage: %s [OPTION]... [FILE]...\n",
"Uncompress FILEs to standard output.\n",
"\n",
" -f, --force force; read compressed data even from a terminal\n",
" -l, --list list compressed file contents\n",
" -q, --quiet suppress all warnings\n",
" -r, --recursive operate recursively on directories\n",
" -S, --suffix=SUF use suffix SUF on compressed files\n",
" --synchronous synchronous output (safer if system crashes, but slower)\n",
" -t, --test test compressed file integrity\n",
" -v, --verbose verbose mode\n",
" --help display this help and exit\n",
" --version display version information and exit\n",
"\n",
"With no FILE, or when FILE is -, read standard input.\n",
"\n",
"Report bugs to <bug-gzip@gnu.org>.\n",
);
pub(super) const SCRIPT_VERSION_FIRST_LINE: &str =
concat!("%s (stuffr ", env!("CARGO_PKG_VERSION"), ") 1.15\n");
pub(super) const SCRIPT_VERSION_REST: &str = concat!(
"Copyright (c) 2026 Thomas Bjork\n",
"License MIT: <https://opensource.org/license/mit>.\n",
"This is stuffr emulating GNU gzip's command-line interface; it contains no GNU code.\n",
"It is provided as is, without warranty of any kind.\n",
"\n",
"Written by Thomas Bjork, for stuffr.\n",
);
pub(super) const INVALID_OPTION: &str = "%s: invalid option -- '%s'\n";
pub(super) const UNRECOGNIZED: &str = "%s: unrecognized option '%s'\n";
pub(super) const AMBIGUOUS: &str = "%s: option '%s' is ambiguous; possibilities:";
pub(super) const CANDIDATE: &str = " '--%s'";
pub(super) const NEEDS_ARGUMENT_SHORT: &str = "%s: option requires an argument -- '%s'\n";
pub(super) const NEEDS_ARGUMENT_LONG: &str = "%s: option '--%s' requires an argument\n";
pub(super) const NO_ARGUMENT_LONG: &str = "%s: option '--%s' doesn't allow an argument\n";
pub(super) const TRY_HELP: &str = "Try `%s --help' for more information.\n";
pub(super) const LZW: &str = "%s: -Z not supported in this version\n";
pub(super) const BITS_NOT_INTEGER: &str = "%s: -b operand is not an integer\n";
pub(super) const SUFFIX_SLASH: &str = "%s: suffix contains '/'\n";
pub(super) const INVALID_SUFFIX: &str = "%s: %s: invalid suffix\n";
pub(super) const ASCII_IGNORED: &str = "%s: option --ascii ignored on this system\n";
pub(super) const OPEN_FAILED: &str = "%s: %s: %s\n";
pub(super) const IS_DIRECTORY: &str = "%s: %s is a directory -- ignored\n";
pub(super) const NOT_REGULAR: &str = "%s: %s is not a directory or a regular file - ignored\n";
pub(super) const HAS_LINKS: &str = "%s: %s has %d other link%s -- file ignored\n";
pub(super) const SET_USER_ID: &str = "%s: %s is set-user-ID on execution - ignored\n";
pub(super) const STICKY: &str = "%s: %s has the sticky bit set - file ignored\n";
pub(super) const HAS_SUFFIX: &str = "%s: %s already has %s suffix -- unchanged\n";
pub(super) const ALREADY_EXISTS: &str = "%s: %s already exists;";
pub(super) const ASK_OVERWRITE: &str = " do you wish to overwrite (y or n)? ";
pub(super) const NOT_OVERWRITTEN: &str = "\tnot overwritten\n";
pub(super) const TIMESTAMP_RANGE: &str =
"%s: %s: warning: file timestamp out of range for gzip format\n";
pub(super) const NOT_TO_TERMINAL: &str = concat!(
"%s: compressed data not written to a terminal. ",
"Use -f to force compression.\n",
"For help, type: %s -h\n"
);
pub(super) const VERBOSE_NAME: &str = "%s:\t";
pub(super) const REPLACED_WITH: &str = " -- replaced with %s\n";
pub(super) const CREATED: &str = " -- created %s\n";
pub(super) const STDOUT_NAME: &[u8] = b"stdout";
pub(super) const STDIN_NAME: &[u8] = b"stdin";
pub(super) const IO_FAILED: &str = "%s: %s: %s\n";
pub(super) const NOT_FROM_TERMINAL: &str = concat!(
"%s: compressed data not read from a terminal. ",
"Use -f to force decompression.\n",
"For help, type: %s -h\n"
);
pub(super) const UNKNOWN_SUFFIX: &str = "%s: %s: unknown suffix -- ignored\n";
pub(super) const NOT_GZIP: &str = "\n%s: %s: not in gzip format\n";
pub(super) const UNEXPECTED_EOF: &str = "\n%s: %s: unexpected end of file\n";
pub(super) const CRC_ERROR: &str = "\n%s: %s: invalid compressed data--crc error\n";
pub(super) const LENGTH_ERROR: &str = "\n%s: %s: invalid compressed data--length error\n";
pub(super) const CRC_LENGTH_ERROR: &str = "\n%s: %s: invalid compressed data--crc/length error\n";
pub(super) const NAME_TOO_LARGE: &str = "\n%s: %s: corrupted input -- file name too large\n";
pub(super) const FORMAT_VIOLATED: &str = "\n%s: %s: invalid compressed data--format violated\n";
pub(super) const UNKNOWN_METHOD: &str = "%s: %s: unknown method %d -- not supported\n";
pub(super) const ENCRYPTED: &str = "%s: %s is encrypted -- not supported\n";
pub(super) const HAS_FLAGS: &str = "%s: %s has flags 0x%s -- not supported\n";
pub(super) const HEADER_CRC: &str = "%s: %s: header checksum 0x%s != computed checksum 0x%s\n";
pub(super) const EXTRA_FIELD: &str = "%s: %s: extra field of %d bytes ignored\n";
pub(super) const TRAILING_GARBAGE: &str = "\n%s: %s: decompression OK, trailing garbage ignored\n";
pub(super) const TRAILING_ZEROS: &str = "\n%s: %s: decompression OK, trailing zero bytes ignored\n";
pub(super) const ZIP_ENCRYPTED: &str = "\n%s: %s: encrypted file -- use unzip\n";
pub(super) const ZIP_METHOD: &str = "\n%s: %s: first entry not deflated or stored -- use unzip\n";
pub(super) const ZIP64: &str = "\n%s: %s: Zip64 entry -- not supported, use unzip\n";
pub(super) const ZIP_INVALID: &str = "\n%s: %s: not a valid zip file\n";
pub(super) const ZIP_MORE: &str = "%s: %s has more than one entry--rest ignored\n";
pub(super) const ZIP_MORE_UNCHANGED: &str = "%s: %s has more than one entry -- unchanged\n";
pub(super) const Z_BITS: &str = "\n%s: %s: compressed with %d bits, can only handle %d bits\n";
pub(super) const Z_FLAGS: &str = "\n%s: %s: warning, unknown flags 0x%s\n";
pub(super) const Z_CORRUPT: &str = "\n%s: %s: corrupt input.\n";
pub(super) const Z_CORRUPT_FILE: &str = "\n%s: %s: corrupt input. Use zcat to recover some data.\n";
pub(super) const PACK_FEW: &str = "\n%s: %s: too few leaves in Huffman tree\n";
pub(super) const PACK_MANY: &str = "\n%s: %s: too many leaves in Huffman tree\n";
pub(super) const PACK_BIT_LENGTH: &str =
"\n%s: %s: invalid compressed data -- Huffman code bit length out of range\n";
pub(super) const CREATE_FAILED: &str = "\n%s: %s: %s\n";
pub(super) const TEST_OK: &str = " OK\n";
pub(super) const LIST_STDIN_NAME: &[u8] = b"stdout";
pub(super) const LIST_HEADER: &str =
" compressed uncompressed ratio uncompressed_name\n";
pub(super) const LIST_HEADER_VERBOSE: &str = concat!(
"method crc date time ",
" compressed uncompressed ratio uncompressed_name\n"
);
pub(super) const LIST_TOTALS: &[u8] = b"(totals)";
pub(super) const METHOD_DEFLATE: &str = "defla";
pub(super) const METHOD_STORED: &str = "store";
pub(super) const METHOD_COMPRESS: &str = "compr";
pub(super) const METHOD_PACK: &str = "pack ";
pub(super) const NO_CRC: &str = "ffffffffffffffff";
pub(super) const MONTHS: [&str; 12] = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
pub(super) fn ratio(bytes_in: u64, body: u64) -> String {
if bytes_in == 0 {
return " -Inf%".to_owned();
}
let r = 100.0 * (bytes_in as f64 - body as f64) / bytes_in as f64;
format!("{r:5.1}%")
}
pub(super) fn quote(name: &[u8]) -> Vec<u8> {
let escaped = |b: u8| !(0x20..0x7f).contains(&b);
let special = |b: u8| b" !\"$&'()*;<=>?[\\^`|:".contains(&b) || escaped(b);
let needs = name.is_empty()
|| name.iter().any(|&b| special(b))
|| matches!(name.first(), Some(b'#' | b'~'))
|| name == b"{"
|| name == b"}";
if !needs {
return name.to_vec();
}
if name.contains(&b'\'') && !name.iter().any(|&b| b"$`\"\\!".contains(&b) || escaped(b)) {
let mut v = Vec::with_capacity(name.len() + 2);
v.push(b'"');
v.extend_from_slice(name);
v.push(b'"');
return v;
}
#[derive(PartialEq)]
enum In {
Single,
Dollar,
}
let mut v = vec![b'\''];
let mut state = In::Single;
for &b in name {
if escaped(b) {
if state == In::Single {
v.extend_from_slice(b"'$'");
state = In::Dollar;
}
match b {
0x07 => v.extend_from_slice(b"\\a"),
0x08 => v.extend_from_slice(b"\\b"),
b'\t' => v.extend_from_slice(b"\\t"),
b'\n' => v.extend_from_slice(b"\\n"),
0x0b => v.extend_from_slice(b"\\v"),
0x0c => v.extend_from_slice(b"\\f"),
b'\r' => v.extend_from_slice(b"\\r"),
_ => v.extend_from_slice(format!("\\{b:03o}").as_bytes()),
}
continue;
}
if state == In::Dollar {
v.extend_from_slice(b"''");
state = In::Single;
}
if b == b'\'' {
v.extend_from_slice(b"'\\''");
} else {
v.push(b);
}
}
v.push(b'\'');
v
}
#[cfg(test)]
mod tests {
use super::*;
fn q(s: &[u8]) -> String {
String::from_utf8(quote(s)).unwrap()
}
#[test]
fn quoting_matches_the_observed_names() {
assert_eq!(q(b"a.txt"), "a.txt");
assert_eq!(q(b""), "''");
assert_eq!(q(b"a b"), "'a b'");
assert_eq!(q(b"it's"), "\"it's\"");
assert_eq!(q(b"it's a"), "\"it's a\"");
assert_eq!(q(b"'"), "\"'\"");
assert_eq!(q(b"a'b$c"), "'a'\\''b$c'");
assert_eq!(q(b"a\tb"), "'a'$'\\t''b'");
assert_eq!(q(b"\n"), "''$'\\n'");
assert_eq!(q(b"a\x01\x02"), "'a'$'\\001\\002'");
assert_eq!(q(b"a\x01b\x02"), "'a'$'\\001''b'$'\\002'");
assert_eq!(q(b"a'\x01"), "'a'\\'''$'\\001'");
assert_eq!(q(b"x\xc3\xa9"), "'x'$'\\303\\251'");
assert_eq!(q(b"a\x7fb"), "'a'$'\\177''b'");
assert_eq!(q(b"~a"), "'~a'");
assert_eq!(q(b"a~"), "a~");
assert_eq!(q(b"#a"), "'#a'");
assert_eq!(q(b"a#"), "a#");
assert_eq!(q(b"{"), "'{'");
assert_eq!(q(b"{}"), "{}");
assert_eq!(q(b"a{b"), "a{b");
assert_eq!(q(b"]"), "]");
assert_eq!(q(b"a=b"), "'a=b'");
assert_eq!(q(b"a:b"), "'a:b'");
assert_eq!(q(b"a^"), "'a^'");
assert_eq!(q(b"-a"), "-a");
assert_eq!(q(b"a%b+c,d.e/f@g_h-i"), "a%b+c,d.e/f@g_h-i");
}
#[test]
fn the_ratio_is_gnus_layout() {
assert_eq!(ratio(6, 8), "-33.3%");
assert_eq!(ratio(24, 11), " 54.2%");
assert_eq!(ratio(1, 3), "-200.0%");
assert_eq!(ratio(0, 2), " -Inf%");
}
#[test]
fn the_version_line_names_stuffr_in_gnus_layout() {
assert!(VERSION_FIRST_LINE.starts_with("gzip (stuffr "));
assert!(VERSION_FIRST_LINE.ends_with(") 1.15\n"));
for text in [LICENSE_REST, WRITTEN_BY, SCRIPT_VERSION_REST] {
for banned in [
"Free Software Foundation",
"GNU General Public",
"Gailly",
"Eggert",
] {
assert!(!text.contains(banned), "{banned}");
}
assert!(text.contains("MIT") || text.contains("Bjork"));
}
}
}