use std::io::{self, Write};
use crate::ObjectId;
const RECORD: u64 = 10240;
const BLOCK: usize = 512;
const NAME: usize = 100;
const PREFIX: usize = 155;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum Kind {
File,
Directory,
Symlink,
GlobalHeader,
ExtendedHeader,
}
impl Kind {
fn flag(self) -> u8 {
match self {
Self::File => b'0',
Self::Directory => b'5',
Self::Symlink => b'2',
Self::GlobalHeader => b'g',
Self::ExtendedHeader => b'x',
}
}
}
pub(super) struct TarWriter<W> {
out: W,
written: u64,
mtime: u64,
}
impl<W: Write> TarWriter<W> {
pub(super) fn new(out: W, mtime: u64) -> Self {
Self {
out,
written: 0,
mtime,
}
}
pub(super) fn commit_header(&mut self, commit: ObjectId) -> io::Result<()> {
let record = pax_record(b"comment", commit.to_string().as_bytes());
self.header(
b"pax_global_header",
0o666,
record.len() as u64,
Kind::GlobalHeader,
b"",
)?;
self.data(&record)
}
pub(super) fn entry(
&mut self,
path: &[u8],
mode: u32,
size: u64,
kind: Kind,
link: &[u8],
id: ObjectId,
) -> io::Result<()> {
let split = split_path(path);
let mut records = Vec::new();
if split.is_none() {
records.extend(pax_record(b"path", path));
}
if link.len() > NAME {
records.extend(pax_record(b"linkpath", link));
}
if !records.is_empty() {
let hex = id.to_string();
self.header(
format!("{hex}.paxheader").as_bytes(),
0o666,
records.len() as u64,
Kind::ExtendedHeader,
b"",
)?;
self.data(&records)?;
}
let data_name = format!("{id}.data");
let (prefix, name) = split.unwrap_or((b"", data_name.as_bytes()));
let see = format!("see {id}.paxheader");
let link = if link.len() > NAME {
see.as_bytes()
} else {
link
};
let mut block = header_block(name, prefix, mode, size, self.mtime, kind, link);
checksum(&mut block);
self.write(&block)
}
pub(super) fn data(&mut self, bytes: &[u8]) -> io::Result<()> {
self.write(bytes)?;
let partial = bytes.len() % BLOCK;
if partial != 0 {
self.write(&[0; BLOCK][partial..])?;
}
Ok(())
}
pub(super) fn finish(mut self) -> io::Result<W> {
let end = (self.written + 2 * BLOCK as u64).div_ceil(RECORD) * RECORD;
let zeros = [0; BLOCK];
while self.written < end {
self.write(&zeros)?;
}
Ok(self.out)
}
fn header(
&mut self,
name: &[u8],
mode: u32,
size: u64,
kind: Kind,
link: &[u8],
) -> io::Result<()> {
let mut block = header_block(name, b"", mode, size, self.mtime, kind, link);
checksum(&mut block);
self.write(&block)
}
fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
self.out.write_all(bytes)?;
self.written += bytes.len() as u64;
Ok(())
}
}
fn split_path(path: &[u8]) -> Option<(&[u8], &[u8])> {
if path.len() <= NAME {
return Some((b"", path));
}
let limit = path.len().min(PREFIX);
let slash = path.get(..limit)?.iter().rposition(|&byte| byte == b'/')?;
let (prefix, name) = (path.get(..slash)?, path.get(slash + 1..)?);
(!name.is_empty() && name.len() <= NAME).then_some((prefix, name))
}
fn pax_record(key: &[u8], value: &[u8]) -> Vec<u8> {
let body = key.len() + value.len() + 3;
let mut length = body + 1;
while length != body + length.to_string().len() {
length = body + length.to_string().len();
}
let mut record = length.to_string().into_bytes();
record.push(b' ');
record.extend_from_slice(key);
record.push(b'=');
record.extend_from_slice(value);
record.push(b'\n');
record
}
fn header_block(
name: &[u8],
prefix: &[u8],
mode: u32,
size: u64,
mtime: u64,
kind: Kind,
link: &[u8],
) -> [u8; BLOCK] {
let mut block = [0u8; BLOCK];
let mut put = |at: usize, bytes: &[u8]| {
if let Some(field) = block.get_mut(at..at + bytes.len()) {
field.copy_from_slice(bytes);
}
};
put(0, name);
put(100, format!("{mode:07o}\0").as_bytes());
put(108, b"0000000\0");
put(116, b"0000000\0");
put(124, format!("{size:011o}\0").as_bytes());
put(136, format!("{mtime:011o}\0").as_bytes());
put(156, &[kind.flag()]);
put(157, link);
put(257, b"ustar\x0000");
put(265, b"root");
put(297, b"root");
put(329, b"0000000\0");
put(337, b"0000000\0");
put(345, prefix);
block
}
fn checksum(block: &mut [u8; BLOCK]) {
if let Some(field) = block.get_mut(148..156) {
field.fill(b' ');
}
let sum: u32 = block.iter().map(|&byte| u32::from(byte)).sum();
if let Some(field) = block.get_mut(148..156) {
field.copy_from_slice(format!("{sum:07o}\0").as_bytes());
}
}
#[cfg(test)]
mod tests {
use super::{pax_record, split_path};
#[test]
fn pax_record_length_counts_itself() {
assert_eq!(pax_record(b"comment", b"x"), b"13 comment=x\n");
let long = vec![b'x'; 120];
let record = pax_record(b"path", &long);
assert!(record.starts_with(b"130 path="));
assert_eq!(record.len(), 130);
}
#[test]
fn splits_at_the_last_fitting_slash() {
let path = format!("{}/{}/{}", "a".repeat(40), "b".repeat(40), "c".repeat(50));
let (prefix, name) = split_path(path.as_bytes()).unwrap();
assert_eq!(name, "c".repeat(50).as_bytes());
assert_eq!(prefix.len(), 81);
assert!(split_path("x".repeat(120).as_bytes()).is_none());
}
}