WIP: Add tar utility

This commit is contained in:
2025-10-16 10:42:41 +03:00
parent 312458b8f0
commit a87c8a7ee2
18 changed files with 712 additions and 33 deletions
+12
View File
@@ -503,6 +503,18 @@ impl Node {
}
}
impl DirectoryData {
fn contains(&self, node: &NodeRef, name: &Filename) -> bool {
{
let cache = self.children.lock();
if cache.iter().any(|(n, _)| n == name) {
return true;
}
}
self.imp.contains(node, name)
}
}
impl HardlinkData {
fn target(&self, link: &NodeRef) -> Result<NodeRef, Error> {
let target = self.imp.target(link)?;
+3
View File
@@ -110,6 +110,9 @@ impl Node {
/// Creates an entry within a directory with given [CreateInfo].
pub fn create(self: &NodeRef, info: CreateInfo, check: AccessToken) -> Result<NodeRef, Error> {
let directory = self.as_directory()?;
if directory.contains(self, info.name) {
return Err(Error::AlreadyExists);
}
let node = directory.imp.create_node(self, &info)?;
self.create_node(node.clone(), info.name, check.clone())?;
+7
View File
@@ -149,6 +149,13 @@ pub trait DirectoryImpl: CommonImpl {
Err(Error::NotImplemented)
}
fn contains(&self, node: &NodeRef, name: &Filename) -> bool {
match self.lookup(node, name) {
Ok(_) => true,
Err(_) => false,
}
}
/// Returns the "length" of the directory in entries
fn len(&self, node: &NodeRef) -> Result<usize, Error> {
let _ = node;
+3 -6
View File
@@ -32,12 +32,9 @@ impl Node {
let directory = self.as_directory()?;
let mut children = directory.children.lock();
// TODO check if an entry already exists with such name
// if children.contains_key(&name) {
// log::warn!("Directory cache already contains an entry: {:?}", name);
// return Err(Error::AlreadyExists);
// }
if children.iter().any(|(n, _)| name == *n) {
return Err(Error::AlreadyExists);
}
assert!(child.parent.replace(Some(self.clone())).is_none());
children.push((name, child));
+8
View File
@@ -2979,6 +2979,13 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "stuff"
version = "0.1.0"
dependencies = [
"bytemuck",
]
[[package]]
name = "subtle"
version = "2.6.1"
@@ -3039,6 +3046,7 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"stuff",
"thiserror 1.0.69",
"tui",
"yggdrasil-abi",
+2
View File
@@ -12,6 +12,7 @@ members = [
"lib/libterm",
"lib/logsink",
"lib/runtime",
"lib/stuff",
"lib/uipc",
"lib/yasync",
"netutils",
@@ -84,6 +85,7 @@ logsink.path = "lib/logsink"
libutil.path = "../lib/libutil"
cryptic.path = "lib/cryptic"
hclient.path = "lib/hclient"
stuff.path = "lib/stuff"
[workspace.lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(rust_analyzer)'] }
+1 -1
View File
@@ -1,4 +1,4 @@
init:1:wait:/sbin/rc default
logd:1:once:/sbin/logd
# user:1:once:/sbin/login /dev/ttyS0
user:1:once:/sbin/login /dev/ttyS0
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "stuff"
version = "0.1.0"
edition = "2021"
[dependencies]
bytemuck.workspace = true
[lints]
workspace = true
+1
View File
@@ -0,0 +1 @@
pub mod tar;
+149
View File
@@ -0,0 +1,149 @@
use std::fmt;
use bytemuck::{Pod, Zeroable};
mod reader;
mod writer;
pub use reader::{TarReader, TarReaderEntry};
pub use writer::{TarFileWriter, TarNewFileMetadata, TarWriter};
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct TarString<const N: usize>([u8; N]);
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct TarOctal<const N: usize>([u8; N]);
#[derive(Zeroable, Pod, Clone, Copy)]
#[repr(C)]
pub struct TarHeader {
pub name: TarString<100>,
pub mode: TarOctal<8>,
pub uid: TarOctal<8>,
pub gid: TarOctal<8>,
pub size: TarOctal<12>,
pub mtime: TarOctal<12>,
pub checksum: TarOctal<8>,
pub file_type: u8,
pub link: TarString<100>,
pub ustar_magic: [u8; 6],
pub ustar_version: [u8; 2],
pub uid_name: TarString<32>,
pub gid_name: TarString<32>,
pub dev_major: TarOctal<8>,
pub dev_minor: TarOctal<8>,
pub filename_prefix: TarString<155>,
_0: [u8; 12],
}
unsafe impl<const N: usize> Pod for TarString<N> {}
unsafe impl<const N: usize> Zeroable for TarString<N> {}
unsafe impl<const N: usize> Pod for TarOctal<N> {}
unsafe impl<const N: usize> Zeroable for TarOctal<N> {}
impl<const N: usize> TarOctal<N> {
pub const fn zero() -> Self {
let mut bytes = [b'0'; N];
bytes[N - 1] = 0;
Self(bytes)
}
pub fn from_u64(value: u64) -> Option<Self> {
// Limit check
if (N - 1) * 3 < 64 && value >= (1 << ((N - 1) * 3)) {
return None;
}
let mut bytes = [0; N];
let mut j = N - 2;
for i in 0..N - 1 {
let v = ((value >> (j * 3)) & 0x7) as u8 + b'0';
bytes[i] = v;
j -= 1;
}
Some(Self(bytes))
}
pub fn from_u32(value: u32) -> Option<Self> {
Self::from_u64(value as u64)
}
pub fn as_usize(&self) -> Option<usize> {
let mut value = 0;
for i in 0..N {
let ch = self.0[i];
if ch == b'\0' || ch == b' ' {
break;
}
if ch < b'0' || ch > b'7' {
return None;
}
value <<= 3;
value |= (ch - b'0') as usize;
}
Some(value)
}
}
impl<const N: usize> TarString<N> {
pub const EMPTY: Self = Self([0; N]);
pub fn new(s: &str) -> Option<Self> {
if s.len() < N {
let mut bytes = [0; N];
bytes[..s.len()].copy_from_slice(s.as_bytes());
Some(Self(bytes))
} else {
None
}
}
pub fn is_empty(&self) -> bool {
self.0[0] == b'\0'
}
pub fn len(&self) -> usize {
self.0.iter().position(|&p| p == b'\0').unwrap_or(N)
}
pub fn as_str(&self) -> Option<&str> {
core::str::from_utf8(&self.0[..self.len()]).ok()
}
}
impl<const N: usize> fmt::Display for TarString<N> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write;
for ch in self.0 {
if ch == b'\0' {
break;
}
f.write_char(ch as char)?;
}
Ok(())
}
}
impl TarHeader {
pub fn is_empty(&self) -> bool {
self.name.is_empty()
}
pub fn is_dir(&self) -> bool {
self.file_type == b'5'
}
pub fn is_file(&self) -> bool {
self.file_type == b'\0' || self.file_type == b'0'
}
}
+80
View File
@@ -0,0 +1,80 @@
use std::io::{self, Read, Seek};
use crate::tar::TarHeader;
pub struct TarReader<R: Read + Seek> {
input: R,
end_of_archive: bool,
position: u64,
}
pub struct TarReaderEntry<'a, R: Read + Seek> {
reader: &'a mut TarReader<R>,
position: u64,
end: u64,
}
impl<R: Read + Seek> TarReader<R> {
pub fn new(input: R) -> Self {
Self {
input,
position: 0,
end_of_archive: false,
}
}
pub fn next(&mut self) -> Result<Option<(TarHeader, TarReaderEntry<'_, R>)>, io::Error> {
let mut have_empty = false;
while !self.end_of_archive {
self.input.seek(io::SeekFrom::Start(self.position))?;
let mut bytes = [0; 512];
self.input.read_exact(&mut bytes)?;
let header = bytemuck::from_bytes::<TarHeader>(&bytes);
if header.is_empty() {
if have_empty {
self.end_of_archive = true;
break;
} else {
have_empty = true;
continue;
}
}
let entry_size = header.size.as_usize().unwrap();
let skip = (entry_size + 1023) & !511;
let position = self.position + 512;
let end = self.position + 512 + entry_size as u64;
self.position += skip as u64;
return Ok(Some((
*header,
TarReaderEntry {
reader: self,
position,
end,
},
)));
}
Ok(None)
}
}
impl<R: Read + Seek> Read for TarReaderEntry<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let amount = buf.len().min((self.end - self.position) as usize);
if amount != 0 {
self.reader.input.seek(io::SeekFrom::Start(self.position))?;
let result = self.reader.input.read(&mut buf[..amount])?;
self.position += result as u64;
Ok(result)
} else {
Ok(0)
}
}
}
+135
View File
@@ -0,0 +1,135 @@
use std::{
fs::File,
io::{self, BufReader, Read, Write},
path::Path,
};
use crate::tar::{TarHeader, TarOctal, TarString};
pub struct TarWriter<W: Write> {
output: W,
}
pub struct TarFileWriter<'a, W: Write> {
writer: &'a mut TarWriter<W>,
written: u64,
size: u64,
}
pub struct TarNewFileMetadata {
pub uid: u32,
pub gid: u32,
pub mode: u32,
}
impl<W: Write> TarWriter<W> {
pub fn new(output: W) -> Self {
Self { output }
}
pub fn finish(&mut self) -> io::Result<()> {
// Two empty blocks
let empty = [0; 512];
self.output.write_all(&empty)?;
self.output.write_all(&empty)?;
self.output.flush()
}
pub fn begin_file(
&mut self,
path: &str,
size: u64,
metadata: TarNewFileMetadata,
) -> io::Result<TarFileWriter<'_, W>> {
// TODO error reporting
// TODO long filenames
let header = TarHeader {
name: TarString::new(path).unwrap(),
mode: TarOctal::from_u32(metadata.mode).unwrap(),
uid: TarOctal::from_u32(metadata.uid).unwrap(),
gid: TarOctal::from_u32(metadata.gid).unwrap(),
size: TarOctal::from_u64(size).unwrap(),
mtime: TarOctal::zero(),
checksum: TarOctal::zero(),
file_type: b'0',
link: TarString::EMPTY,
ustar_magic: *b"ustar\0",
ustar_version: *b"00",
uid_name: TarString::EMPTY,
gid_name: TarString::EMPTY,
dev_major: TarOctal::zero(),
dev_minor: TarOctal::zero(),
filename_prefix: TarString::EMPTY,
_0: [0; 12],
};
self.output.write_all(bytemuck::bytes_of(&header))?;
Ok(TarFileWriter {
writer: self,
written: 0,
size,
})
}
pub fn append_file<P: AsRef<Path>>(
&mut self,
path: P,
path_in_archive: &str,
) -> io::Result<()> {
let file = File::open(path)?;
let metadata = file.metadata()?;
let mut reader = BufReader::new(file);
// TODO
let tar_metadata = TarNewFileMetadata {
uid: 0,
gid: 0,
mode: 0o644,
};
let mut writer = self.begin_file(path_in_archive, metadata.len(), tar_metadata)?;
let mut buffer = [0; 8192];
loop {
let len = reader.read(&mut buffer)?;
if len == 0 {
break;
}
writer.write_all(&buffer[..len])?;
}
writer.finish()?;
Ok(())
}
}
impl<W: Write> TarFileWriter<'_, W> {
pub fn finish(mut self) -> io::Result<()> {
let aligned_size = (self.size + 511) & !511;
if self.written < aligned_size {
let zeroes = aligned_size - self.written;
// TODO suboptimal, but don't want Seek on writer?
for _ in 0..zeroes {
self.writer.output.write_all(&[0])?;
}
self.writer.output.flush()?;
self.written = self.size;
}
Ok(())
}
}
impl<W: Write> Write for TarFileWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let can_write = (self.size - self.written).min(buf.len() as u64);
if can_write != 0 {
let len = self.writer.output.write(&buf[..can_write as usize])?;
self.written += len as u64;
Ok(len)
} else {
Ok(0)
}
}
fn flush(&mut self) -> io::Result<()> {
self.writer.output.flush()
}
}
+5
View File
@@ -21,6 +21,7 @@ serde_json.workspace = true
sha2.workspace = true
chrono.workspace = true
tui.workspace = true
stuff.workspace = true
# Own regex implementation?
regex = "1.11.1"
@@ -77,6 +78,10 @@ path = "src/kmod.rs"
name = "echo"
path = "src/echo.rs"
[[bin]]
name = "tar"
path = "src/tar.rs"
[[bin]]
name = "ls"
path = "src/ls.rs"
+246
View File
@@ -0,0 +1,246 @@
use std::{
fmt,
fs::{self, File},
io::{self, stdout, BufWriter, Read, Seek, Write},
path::{Path, PathBuf},
process::ExitCode,
};
use clap::Parser;
use stuff::tar::{TarHeader, TarReader, TarReaderEntry, TarWriter};
#[derive(Debug, Parser)]
struct Args {
#[clap(short = 'C')]
chdir: Option<PathBuf>,
#[clap(short = 'c')]
create: bool,
#[clap(short = 't')]
print: bool,
#[clap(short = 'x')]
extract: bool,
#[clap(short = 'f')]
archive: PathBuf,
inputs: Vec<PathBuf>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Action {
Create,
Print,
Extract,
}
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("{0}: {1}")]
Io(PathBuf, io::Error),
#[error("No action specified, use -c/-t/-x")]
NoAction,
#[error("Multiple actions specified, use only -c/-t/-x")]
MultipleActions,
}
trait ContextualizeError {
type Output;
fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output;
}
impl<T, E: ContextualizeError> ContextualizeError for Result<T, E> {
type Output = Result<T, <E as ContextualizeError>::Output>;
fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output {
match self {
Ok(val) => Ok(val),
Err(error) => Err(error.contextualize(p)),
}
}
}
impl ContextualizeError for io::Error {
type Output = Error;
fn contextualize<P: Into<PathBuf>>(self, p: P) -> Self::Output {
Error::Io(p.into(), self)
}
}
fn create_archive(
output: impl AsRef<Path>,
inputs: &[PathBuf],
chdir: Option<&PathBuf>,
) -> Result<(), Error> {
fn write_path<W: Write>(
writer: &mut TarWriter<W>,
fs_path: &Path,
archive_path: &Path,
) -> Result<(), Error> {
if fs_path.is_file() {
println!("{}", archive_path.display());
let archive_path_str = archive_path.to_str().unwrap();
writer
.append_file(fs_path, archive_path_str)
.contextualize(fs_path)?;
Ok(())
} else if fs_path.is_dir() {
let mut dir = fs::read_dir(fs_path).contextualize(fs_path)?;
for entry in dir {
let entry = entry.contextualize(fs_path)?;
let filename = entry.file_name();
if filename.eq_ignore_ascii_case(".") || filename.eq_ignore_ascii_case("..") {
continue;
}
write_path(
writer,
&fs_path.join(&filename),
&archive_path.join(&filename),
)?;
}
Ok(())
} else {
todo!()
}
}
if inputs.is_empty() {
todo!();
}
let archive = output.as_ref();
let mut writer = File::create(archive)
.map(BufWriter::new)
.map(TarWriter::new)
.contextualize(archive)?;
let chdir = match chdir {
Some(chdir) => chdir.as_path(),
None => ".".as_ref(),
};
for input in inputs {
if input.is_absolute() || input.starts_with("/") {
// fs path == archive path
write_path(&mut writer, input, input)?;
} else {
todo!();
// write_path(&mut writer, chdir, chdir.join(input).as_path())?;
}
}
writer.finish().contextualize(archive)?;
Ok(())
}
fn print_archive(archive: impl AsRef<Path>) -> Result<(), Error> {
let archive = archive.as_ref();
let input = File::open(archive).contextualize(archive)?;
let mut reader = TarReader::new(input);
loop {
let Some((header, _)) = reader.next().contextualize(archive)? else {
break;
};
println!("{}", header.name);
}
Ok(())
}
fn extract_archive(archive: impl AsRef<Path>, chdir: Option<&PathBuf>) -> Result<(), Error> {
fn extract_file<R: Read + Seek>(
entry: &mut TarReaderEntry<R>,
header: &TarHeader,
chdir: Option<&PathBuf>,
) -> Result<(), Error> {
// TODO path sanitization
let Some(raw_path) = header.name.as_str() else {
eprintln!("Skipping invalid path {}", header.name);
return Ok(());
};
let sanitized_path = raw_path.trim_start_matches('/');
let sanitized_path = match chdir {
Some(chdir) => chdir.join(sanitized_path),
None => sanitized_path.into(),
};
println!("{}", sanitized_path.display());
if header.is_file() {
if let Some(parent) = sanitized_path.parent() {
std::fs::create_dir_all(parent).contextualize(parent)?;
}
// Write the file
let mut output = File::create(&sanitized_path)
.map(BufWriter::new)
.contextualize(&sanitized_path)?;
let mut buffer = [0; 4096];
loop {
let len = entry.read(&mut buffer).contextualize(raw_path)?;
if len == 0 {
break;
}
output
.write_all(&buffer[..len])
.contextualize(&sanitized_path)?;
}
Ok(())
} else if header.is_dir() {
std::fs::create_dir_all(&sanitized_path).contextualize(sanitized_path)
} else {
Ok(())
}
}
let archive = archive.as_ref();
let input = File::open(archive).contextualize(archive)?;
let mut reader = TarReader::new(input);
loop {
let Some((header, mut entry)) = reader.next().contextualize(archive)? else {
break;
};
extract_file(&mut entry, &header, chdir)
.inspect_err(|e| eprintln!("{e}"))
.ok();
}
Ok(())
}
fn run(args: &Args) -> Result<(), Error> {
let action = match (args.create, args.print, args.extract) {
(false, false, false) => return Err(Error::NoAction),
(true, false, false) => Action::Create,
(false, true, false) => Action::Print,
(false, false, true) => Action::Extract,
(_, _, _) => return Err(Error::MultipleActions),
};
match action {
Action::Create => create_archive(&args.archive, &args.inputs, args.chdir.as_ref()),
Action::Print => print_archive(&args.archive),
Action::Extract => extract_archive(&args.archive, args.chdir.as_ref()),
}
}
fn main() -> ExitCode {
let args = Args::parse();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
+23 -25
View File
@@ -1,30 +1,28 @@
use std::{collections::VecDeque, fmt::Write};
use std::{sync::mpsc, time::Duration};
use libterm::{Clear, Term, TermKey};
fn producer(tx: mpsc::Sender<u32>) {
println!("Producer started");
for i in 0..10 {
std::thread::sleep(Duration::from_secs(1));
tx.send(i).ok();
}
println!("Producer finished");
}
fn consumer(rx: mpsc::Receiver<u32>) {
println!("Consumer started");
while let Ok(msg) = rx.recv() {
println!("Consumer: {msg}");
}
println!("Consumer finished");
}
fn main() {
let mut term = Term::open().expect("open term");
let (tx, rx) = mpsc::channel();
let jh0 = std::thread::spawn(move || producer(tx));
let jh1 = std::thread::spawn(move || consumer(rx));
let mut keys = VecDeque::new();
loop {
term.clear(Clear::All).ok();
for (i, key) in keys.iter().enumerate() {
term.set_cursor_position(i, 0).ok();
write!(term, "{key:?}").ok();
}
term.flush().ok();
let key = term.read_key().unwrap();
if key == TermKey::Eof {
break;
}
keys.push_front(key);
if keys.len() >= 20 {
keys.pop_back();
}
}
jh0.join().unwrap();
jh1.join().unwrap();
println!("Finished");
}
+26 -1
View File
@@ -10,6 +10,24 @@ enum Outcome {
Data(usize),
}
struct Readline<'a> {
stdin: &'a mut TerminalInput,
stdout: &'a mut Stdout,
buffer: &'a mut String,
}
impl<'a> Readline<'a> {
fn new(stdin: &'a mut TerminalInput, stdout: &'a mut Stdout, buffer: &'a mut String) -> Self {
Self {
stdin,
stdout,
buffer,
}
}
fn handle_char(&mut self, ch: char) {}
}
fn readline_inner(
stdin: &mut TerminalInput,
stdout: &mut Stdout,
@@ -50,7 +68,14 @@ fn readline_inner(
}
// Tab
'\t' => {
log::info!("Tab");
// TODO parse the buffer into a command struct and do ops on the command
// or at least check the token/word before current to see if it's a
// separator like ';', '&&' etc
let (is_command, last_word) = match buffer.rsplit_once(' ') {
Some((_, last)) => (true, last),
None => (false, buffer.trim()),
};
log::info!("Tab {last_word:?}");
}
ch if ch.is_whitespace() || ch.is_ascii_graphic() => {
let bytes = ch.encode_utf8(&mut ch_buffer);
+1
View File
@@ -30,6 +30,7 @@ const PROGRAMS: &[(&str, &str)] = &[
("shell", "bin/sh"),
// sysutils
("cat", "bin/cat"),
("tar", "bin/tar"),
("chmod", "bin/chmod"),
("chroot", "sbin/chroot"),
("date", "bin/date"),