///| Pure tar archive generation helpers (USTAR format).
///|
/// Format an Int64 value as a zero-padded octal string of the given width.
pub fn tar_format_octal(value : Int64, width : Int) -> String {
if value == 0L {
let buf = StringBuilder::new()
for _i in 0.. 0L {
let d = (v % 8L).to_int()
digits.push((d + '0'.to_int()).unsafe_to_char())
v = v / 8L
}
// Reverse
let buf = StringBuilder::new()
let digit_len = digits.length()
// Pad with leading zeros
if digit_len < width {
let pad_count = width - digit_len
for _i in 0..= 0 {
buf.write_char(digits[di])
di -= 1
}
buf.to_string()
}
///|
/// Write a UTF-8 string into a byte buffer at the given offset, up to max_len bytes.
pub fn tar_write_string(
buf : FixedArray[Byte],
offset : Int,
s : String,
max_len : Int,
) -> Unit {
let bytes = @utf8.encode(s)
let len = if bytes.length() < max_len { bytes.length() } else { max_len }
for i in 0.. (String, String) {
// Find a '/' boundary to split into prefix (max 155) and name (max 100)
let mut best_slash = -1
for i in 0..= 0 {
let prefix_part = path[:best_slash].to_owned()
let name_part = path[best_slash + 1:].to_owned()
(name_part, prefix_part)
} else {
// Can't split; truncate name
(path[:100].to_owned(), "")
}
}
///|
/// Build a 512-byte USTAR tar header.
///
/// - `path`: file path (split into prefix+name if > 100 chars)
/// - `mode`: Unix file mode (e.g. 0o100644)
/// - `size`: file content size in bytes
/// - `mtime`: modification time as Unix timestamp
/// - `typeflag`: tar type flag byte (b'0' = regular, b'5' = directory, b'2' = symlink)
/// - `linkname`: symlink target (empty for non-symlinks)
pub fn tar_make_header(
path : String,
mode : Int,
size : Int,
mtime : Int64,
typeflag : Byte,
linkname : String,
) -> FixedArray[Byte] {
let header = FixedArray::make(512, b'\x00')
// Split path into prefix + name if needed
let (name_str, prefix_str) = if path.length() > 100 {
tar_split_path(path)
} else {
(path, "")
}
// name (offset 0, 100 bytes)
tar_write_string(header, 0, name_str, 100)
// mode (offset 100, 8 bytes) — octal NUL-terminated
let mode_str = tar_format_octal(mode.to_int64(), 7)
tar_write_string(header, 100, mode_str, 8)
// uid (offset 108, 8 bytes)
tar_write_string(header, 108, "0000000", 8)
// gid (offset 116, 8 bytes)
tar_write_string(header, 116, "0000000", 8)
// size (offset 124, 12 bytes) — octal NUL-terminated
let size_str = tar_format_octal(size.to_int64(), 11)
tar_write_string(header, 124, size_str, 12)
// mtime (offset 136, 12 bytes) — octal NUL-terminated
let mtime_str = tar_format_octal(mtime, 11)
tar_write_string(header, 136, mtime_str, 12)
// checksum placeholder (offset 148, 8 bytes) — spaces for computation
for ci in 0..<8 {
header[148 + ci] = b' '
}
// typeflag (offset 156, 1 byte)
header[156] = typeflag
// linkname (offset 157, 100 bytes)
if linkname.length() > 0 {
tar_write_string(header, 157, linkname, 100)
}
// magic (offset 257, 6 bytes) "ustar\0"
tar_write_string(header, 257, "ustar", 6)
// version (offset 263, 2 bytes) "00"
header[263] = b'0'
header[264] = b'0'
// uname (offset 265, 32 bytes)
tar_write_string(header, 265, "root", 32)
// gname (offset 297, 32 bytes)
tar_write_string(header, 297, "root", 32)
// devmajor (offset 329, 8 bytes)
tar_write_string(header, 329, "0000000", 8)
// devminor (offset 337, 8 bytes)
tar_write_string(header, 337, "0000000", 8)
// prefix (offset 345, 155 bytes)
if prefix_str.length() > 0 {
tar_write_string(header, 345, prefix_str, 155)
}
// Compute checksum
let mut chksum = 0
for ci in 0..<512 {
chksum += header[ci].to_int()
}
// Write checksum as 6-digit octal + NUL + space
let chksum_str = tar_format_octal(chksum.to_int64(), 6)
for ci in 0..<6 {
header[148 + ci] = chksum_str[ci].to_int().to_byte()
}
header[148 + 6] = b'\x00'
header[148 + 7] = b' '
header
}
///|
/// Parse the committer timestamp from raw commit object bytes.
/// Returns the Unix timestamp as Int64, or 0 if not found.
pub fn tar_parse_committer_timestamp(data : Bytes) -> Int64 {
let text = @utf8.decode_lossy(data[:])
for line_view in text.split("\n") {
let line = line_view.to_owned()
if line.length() == 0 {
break
}
if line.has_prefix("committer ") {
// Format: "committer Name timestamp tz"
let rest = line[10:].to_owned()
// Find '>' to locate end of email
let mut gt_pos = -1
for ci in 0..' {
gt_pos = ci
}
}
if gt_pos >= 0 && gt_pos + 2 < rest.length() {
let after_email = rest[gt_pos + 2:].to_owned()
// "timestamp tz"
let parts = after_email.split(" ")
let parts_arr : Array[String] = []
for p in parts {
parts_arr.push(p.to_owned())
}
if parts_arr.length() >= 1 {
let ts_str = parts_arr[0]
let ts = @string.parse_int64(ts_str) catch { _ => 0L }
return ts
}
}
}
}
0L
}
///|
/// Emit parent directory entries for a tar path into the output buffer.
/// Tracks already-emitted directories via the `emitted` map.
pub fn tar_emit_parent_dirs(
out : Array[Byte],
path : String,
mtime : Int64,
emitted : Map[String, Bool],
) -> Unit {
// Collect all parent directories
let dirs : Array[String] = []
let mut p = path
while true {
let mut slash_pos = -1
for i in 0..= 0 {
let dir = dirs[di]
if !emitted.contains(dir) {
emitted[dir] = true
let hdr = tar_make_header(dir, 0o40755, 0, mtime, b'5', "")
for b in hdr {
out.push(b)
}
}
di -= 1
}
}