use alloc::vec::Vec;
use crate::io;
use super::{get_arg, open_read, open_write_create, create_parent_dirs, mode_string};
#[repr(C)]
pub struct TarHeader {
pub name: [u8; 100],
pub mode: [u8; 8],
pub uid: [u8; 8],
pub gid: [u8; 8],
pub size: [u8; 12],
pub mtime: [u8; 12],
pub checksum: [u8; 8],
pub typeflag: u8,
pub linkname: [u8; 100],
pub magic: [u8; 6],
pub version: [u8; 2],
pub uname: [u8; 32],
pub gname: [u8; 32],
pub devmajor: [u8; 8],
pub devminor: [u8; 8],
pub prefix: [u8; 155],
pub _pad: [u8; 12],
}
impl TarHeader {
pub fn new() -> Self {
Self {
name: [0; 100],
mode: [0; 8],
uid: [0; 8],
gid: [0; 8],
size: [0; 12],
mtime: [0; 12],
checksum: [0; 8],
typeflag: 0,
linkname: [0; 100],
magic: [0; 6],
version: [0; 2],
uname: [0; 32],
gname: [0; 32],
devmajor: [0; 8],
devminor: [0; 8],
prefix: [0; 155],
_pad: [0; 12],
}
}
pub fn is_empty(&self) -> bool {
self.name[0] == 0
}
pub fn get_name(&self) -> &[u8] {
let len = self.name.iter().position(|&c| c == 0).unwrap_or(100);
&self.name[..len]
}
pub fn get_size(&self) -> u64 {
parse_octal(&self.size)
}
pub fn get_mode(&self) -> u32 {
parse_octal(&self.mode) as u32
}
pub fn compute_checksum(&self) -> u32 {
let bytes = unsafe {
core::slice::from_raw_parts(self as *const _ as *const u8, 512)
};
let mut sum = 0u32;
for (i, &b) in bytes.iter().enumerate() {
if i >= 148 && i < 156 {
sum += b' ' as u32;
} else {
sum += b as u32;
}
}
sum
}
pub fn set_checksum(&mut self) {
self.checksum = *b" ";
let sum = self.compute_checksum();
write_octal(&mut self.checksum[..6], sum as u64);
self.checksum[6] = 0;
self.checksum[7] = b' ';
}
}
fn parse_octal(bytes: &[u8]) -> u64 {
let mut result = 0u64;
for &b in bytes {
if b == 0 || b == b' ' { break; }
if b >= b'0' && b <= b'7' {
result = result * 8 + (b - b'0') as u64;
}
}
result
}
fn write_octal(buf: &mut [u8], mut val: u64) {
let len = buf.len();
for i in (0..len).rev() {
buf[i] = b'0' + (val & 7) as u8;
val >>= 3;
}
}
pub fn tar(argc: i32, argv: *const *const u8) -> i32 {
let mut create = false;
let mut extract = false;
let mut list = false;
let mut verbose = false;
let mut archive_file: Option<&[u8]> = None;
let mut files: Vec<&[u8]> = Vec::new();
let mut i = 1;
while i < argc {
if let Some(arg) = unsafe { get_arg(argv, i) } {
if arg.starts_with(b"-") || (i == 1 && !arg.starts_with(b"/")) {
let opts = if arg.starts_with(b"-") { &arg[1..] } else { arg };
for &c in opts {
match c {
b'c' => create = true,
b'x' => extract = true,
b't' => list = true,
b'v' => verbose = true,
b'f' => {
i += 1;
if let Some(f) = unsafe { get_arg(argv, i) } {
archive_file = Some(f);
}
}
b'z' | b'j' | b'J' => {
}
_ => {}
}
}
} else {
files.push(arg);
}
}
i += 1;
}
let archive = match archive_file {
Some(f) => f,
None => {
io::write_str(2, b"tar: -f archive required\n");
return 1;
}
};
if create {
tar_create(archive, &files, verbose)
} else if extract {
tar_extract(archive, &files, verbose)
} else if list {
tar_list(archive, verbose)
} else {
io::write_str(2, b"tar: specify -c, -x, or -t\n");
1
}
}
fn tar_create(archive: &[u8], files: &[&[u8]], verbose: bool) -> i32 {
let fd = open_write_create(archive, 0o644);
if fd < 0 {
io::write_str(2, b"tar: cannot create archive\n");
return 1;
}
for &file in files {
if tar_add_file(fd, file, verbose) != 0 {
io::close(fd);
return 1;
}
}
let empty = [0u8; 512];
io::write_all(fd, &empty);
io::write_all(fd, &empty);
io::close(fd);
0
}
fn tar_add_file(fd: i32, path: &[u8], verbose: bool) -> i32 {
let mut stat_buf: libc::stat = unsafe { core::mem::zeroed() };
let mut path_z = [0u8; 256];
let len = path.len().min(255);
path_z[..len].copy_from_slice(&path[..len]);
if unsafe { libc::stat(path_z.as_ptr() as *const i8, &mut stat_buf) } != 0 {
io::write_str(2, b"tar: cannot stat ");
io::write_all(2, path);
io::write_str(2, b"\n");
return 1;
}
let mode = stat_buf.st_mode;
let is_dir = (mode & libc::S_IFMT) == libc::S_IFDIR;
let is_link = (mode & libc::S_IFMT) == libc::S_IFLNK;
if is_dir {
tar_add_dir_entry(fd, path, &stat_buf, verbose);
let dir = unsafe { libc::opendir(path_z.as_ptr() as *const i8) };
if !dir.is_null() {
loop {
let entry = unsafe { libc::readdir(dir) };
if entry.is_null() { break; }
let name = unsafe { io::cstr_to_slice((*entry).d_name.as_ptr() as *const u8) };
if name == b"." || name == b".." { continue; }
let mut full_path = Vec::new();
full_path.extend_from_slice(path);
if !path.ends_with(b"/") {
full_path.push(b'/');
}
full_path.extend_from_slice(name);
tar_add_file(fd, &full_path, verbose);
}
unsafe { libc::closedir(dir) };
}
} else if is_link {
tar_add_link_entry(fd, path, &stat_buf, verbose);
} else {
tar_add_file_entry(fd, path, &stat_buf, verbose);
}
0
}
fn tar_add_dir_entry(fd: i32, path: &[u8], stat: &libc::stat, verbose: bool) {
let mut header = TarHeader::new();
let len = path.len().min(99);
header.name[..len].copy_from_slice(&path[..len]);
if len < 99 && !path.ends_with(b"/") {
header.name[len] = b'/';
}
write_octal(&mut header.mode[..7], (stat.st_mode & 0o7777) as u64);
write_octal(&mut header.uid[..7], stat.st_uid as u64);
write_octal(&mut header.gid[..7], stat.st_gid as u64);
write_octal(&mut header.size[..11], 0);
write_octal(&mut header.mtime[..11], stat.st_mtime as u64);
header.typeflag = b'5';
header.magic = *b"ustar ";
header.version = *b" \0";
header.set_checksum();
let header_bytes = unsafe {
core::slice::from_raw_parts(&header as *const _ as *const u8, 512)
};
io::write_all(fd, header_bytes);
if verbose {
io::write_all(1, path);
io::write_str(1, b"/\n");
}
}
fn tar_add_link_entry(fd: i32, path: &[u8], stat: &libc::stat, verbose: bool) {
let mut header = TarHeader::new();
let len = path.len().min(100);
header.name[..len].copy_from_slice(&path[..len]);
write_octal(&mut header.mode[..7], (stat.st_mode & 0o7777) as u64);
write_octal(&mut header.uid[..7], stat.st_uid as u64);
write_octal(&mut header.gid[..7], stat.st_gid as u64);
write_octal(&mut header.size[..11], 0);
write_octal(&mut header.mtime[..11], stat.st_mtime as u64);
let mut path_z = [0u8; 256];
let plen = path.len().min(255);
path_z[..plen].copy_from_slice(&path[..plen]);
let mut linkbuf = [0u8; 100];
let link_len = unsafe {
libc::readlink(path_z.as_ptr() as *const i8, linkbuf.as_mut_ptr() as *mut i8, 100)
};
if link_len > 0 {
header.linkname[..link_len as usize].copy_from_slice(&linkbuf[..link_len as usize]);
}
header.typeflag = b'2';
header.magic = *b"ustar ";
header.version = *b" \0";
header.set_checksum();
let header_bytes = unsafe {
core::slice::from_raw_parts(&header as *const _ as *const u8, 512)
};
io::write_all(fd, header_bytes);
if verbose {
io::write_all(1, path);
io::write_str(1, b"\n");
}
}
fn tar_add_file_entry(fd: i32, path: &[u8], stat: &libc::stat, verbose: bool) {
let mut header = TarHeader::new();
let len = path.len().min(100);
header.name[..len].copy_from_slice(&path[..len]);
write_octal(&mut header.mode[..7], (stat.st_mode & 0o7777) as u64);
write_octal(&mut header.uid[..7], stat.st_uid as u64);
write_octal(&mut header.gid[..7], stat.st_gid as u64);
let size = stat.st_size as u64;
write_octal(&mut header.size[..11], size);
write_octal(&mut header.mtime[..11], stat.st_mtime as u64);
header.typeflag = b'0';
header.magic = *b"ustar ";
header.version = *b" \0";
header.set_checksum();
let header_bytes = unsafe {
core::slice::from_raw_parts(&header as *const _ as *const u8, 512)
};
io::write_all(fd, header_bytes);
let mut path_z = [0u8; 256];
let plen = path.len().min(255);
path_z[..plen].copy_from_slice(&path[..plen]);
let file_fd = open_read(&path_z);
if file_fd >= 0 {
let mut buf = [0u8; 512];
let mut remaining = size;
while remaining > 0 {
let to_read = remaining.min(512) as usize;
let n = io::read(file_fd, &mut buf[..to_read]);
if n <= 0 { break; }
if (n as usize) < 512 {
for i in (n as usize)..512 {
buf[i] = 0;
}
}
io::write_all(fd, &buf);
remaining -= n as u64;
}
io::close(file_fd);
}
if verbose {
io::write_all(1, path);
io::write_str(1, b"\n");
}
}
fn tar_extract(archive: &[u8], files: &[&[u8]], verbose: bool) -> i32 {
let fd = open_read(archive);
if fd < 0 {
io::write_str(2, b"tar: cannot open archive\n");
return 1;
}
let mut header_buf = [0u8; 512];
loop {
if io::read(fd, &mut header_buf) != 512 {
break;
}
let header = unsafe { &*(header_buf.as_ptr() as *const TarHeader) };
if header.is_empty() {
break;
}
let name = header.get_name();
let size = header.get_size();
let mode = header.get_mode();
let should_extract = files.is_empty() || files.iter().any(|&f| {
name.starts_with(f) || f == name
});
if should_extract {
match header.typeflag {
b'5' | b'\0' if name.ends_with(b"/") => {
let mut path_z = [0u8; 256];
let len = name.len().min(255);
path_z[..len].copy_from_slice(&name[..len]);
unsafe { libc::mkdir(path_z.as_ptr() as *const i8, mode) };
if verbose {
io::write_all(1, name);
io::write_str(1, b"\n");
}
}
b'2' => {
let target = {
let len = header.linkname.iter().position(|&c| c == 0).unwrap_or(100);
&header.linkname[..len]
};
let mut name_z = [0u8; 256];
let mut target_z = [0u8; 256];
let nlen = name.len().min(255);
let tlen = target.len().min(255);
name_z[..nlen].copy_from_slice(&name[..nlen]);
target_z[..tlen].copy_from_slice(&target[..tlen]);
unsafe { libc::symlink(target_z.as_ptr() as *const i8, name_z.as_ptr() as *const i8) };
if verbose {
io::write_all(1, name);
io::write_str(1, b"\n");
}
}
b'0' | b'\0' => {
extract_file(name, fd, size, mode, verbose);
}
_ => {
}
}
}
if size > 0 && (header.typeflag == b'0' || header.typeflag == 0) {
let blocks = (size + 511) / 512;
if !should_extract {
for _ in 0..blocks {
io::read(fd, &mut header_buf);
}
}
}
}
io::close(fd);
0
}
fn extract_file(name: &[u8], archive_fd: i32, size: u64, mode: u32, verbose: bool) {
create_parent_dirs(name);
let mut path_z = [0u8; 256];
let len = name.len().min(255);
path_z[..len].copy_from_slice(&name[..len]);
let fd = open_write_create(&path_z, mode as i32);
if fd < 0 {
io::write_str(2, b"tar: cannot create ");
io::write_all(2, name);
io::write_str(2, b"\n");
let blocks = (size + 511) / 512;
let mut skip_buf = [0u8; 512];
for _ in 0..blocks {
io::read(archive_fd, &mut skip_buf);
}
return;
}
let mut remaining = size;
let mut buf = [0u8; 512];
while remaining > 0 {
if io::read(archive_fd, &mut buf) != 512 {
break;
}
let to_write = remaining.min(512) as usize;
io::write_all(fd, &buf[..to_write]);
remaining -= to_write as u64;
}
io::close(fd);
if verbose {
io::write_all(1, name);
io::write_str(1, b"\n");
}
}
fn tar_list(archive: &[u8], verbose: bool) -> i32 {
let fd = open_read(archive);
if fd < 0 {
io::write_str(2, b"tar: cannot open archive\n");
return 1;
}
let mut header_buf = [0u8; 512];
loop {
if io::read(fd, &mut header_buf) != 512 {
break;
}
let header = unsafe { &*(header_buf.as_ptr() as *const TarHeader) };
if header.is_empty() {
break;
}
let name = header.get_name();
let size = header.get_size();
if verbose {
let mode = header.get_mode();
let type_char = match header.typeflag {
b'5' => b'd',
b'2' => b'l',
_ => b'-',
};
io::write_all(1, &[type_char]);
io::write_all(1, &mode_string(mode));
io::write_str(1, b" ");
io::write_num(1, size);
io::write_str(1, b" ");
}
io::write_all(1, name);
io::write_str(1, b"\n");
if size > 0 && (header.typeflag == b'0' || header.typeflag == 0) {
let blocks = (size + 511) / 512;
for _ in 0..blocks {
io::read(fd, &mut header_buf);
}
}
}
io::close(fd);
0
}
#[cfg(test)]
mod tests {
extern crate std;
use std::process::Command;
use std::path::PathBuf;
fn get_armybox_path() -> PathBuf {
if let Ok(path) = std::env::var("ARMYBOX_PATH") {
return PathBuf::from(path);
}
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap());
let release = manifest_dir.join("target/release/armybox");
if release.exists() { return release; }
manifest_dir.join("target/debug/armybox")
}
#[test]
fn test_tar_no_archive() {
let armybox = get_armybox_path();
if !armybox.exists() { return; }
let output = Command::new(&armybox)
.args(["tar", "-c"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("-f archive required"));
}
#[test]
fn test_tar_no_mode() {
let armybox = get_armybox_path();
if !armybox.exists() { return; }
let output = Command::new(&armybox)
.args(["tar", "-f", "test.tar"])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(1));
let stderr = std::string::String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("specify -c, -x, or -t"));
}
#[test]
fn test_tar_create_and_list() {
let armybox = get_armybox_path();
if !armybox.exists() { return; }
let dir = std::env::temp_dir().join("armybox_tar_test");
let _ = std::fs::create_dir_all(&dir);
std::fs::write(dir.join("testfile.txt"), "hello world").unwrap();
let tar_path = dir.join("test.tar");
let output = Command::new(&armybox)
.args(["tar", "-cf", tar_path.to_str().unwrap(),
dir.join("testfile.txt").to_str().unwrap()])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0));
let output = Command::new(&armybox)
.args(["tar", "-tf", tar_path.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = std::string::String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("testfile.txt"));
let _ = std::fs::remove_dir_all(&dir);
}
}