use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use anyhow::{Result, anyhow};
use arrow::array::{StringArray, StringBuilder, UInt8Array, UInt8Builder, UInt32Array, UInt32Builder};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use crate::index::{
FORMAT_VERSION_KEY, ZNIPPY_FORMAT_VERSION, ZNIPPY_FSMETA_MODULE, check_format_version,
read_reserved_section_bytes,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FsKind {
Dir = 0,
File = 1,
FileDefaultMode = 2,
}
impl FsKind {
fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::Dir),
1 => Some(Self::File),
2 => Some(Self::FileDefaultMode),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsEntry {
pub relative_path: String,
pub kind: FsKind,
pub mode: u32,
}
pub fn fsmeta_schema() -> Arc<Schema> {
let fields = vec![
Field::new("relative_path", DataType::Utf8, false),
Field::new("kind", DataType::UInt8, false),
Field::new("mode", DataType::UInt32, false),
];
let mut md = HashMap::new();
md.insert(FORMAT_VERSION_KEY.to_string(), ZNIPPY_FORMAT_VERSION.to_string());
Arc::new(Schema::new_with_metadata(fields, md))
}
pub fn build_fsmeta_batch(entries: &[FsEntry]) -> Result<RecordBatch> {
let mut order: Vec<&FsEntry> = entries.iter().collect();
order.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
let n = order.len();
let mut path_b = StringBuilder::with_capacity(n, n * 48);
let mut kind_b = UInt8Builder::with_capacity(n);
let mut mode_b = UInt32Builder::with_capacity(n);
for e in order {
path_b.append_value(&e.relative_path);
kind_b.append_value(e.kind as u8);
mode_b.append_value(e.mode);
}
RecordBatch::try_new(
fsmeta_schema(),
vec![Arc::new(path_b.finish()), Arc::new(kind_b.finish()), Arc::new(mode_b.finish())],
)
.map_err(|e| anyhow!("fsmeta batch: {e}"))
}
fn decode_fsmeta_section(bytes: &[u8]) -> Result<Vec<FsEntry>> {
use arrow::ipc::reader::StreamReader;
let reader = StreamReader::try_new(std::io::Cursor::new(bytes), None)
.map_err(|e| anyhow!("fsmeta section: not a readable Arrow stream: {e}"))?;
check_format_version(reader.schema().metadata())?;
let mut out = Vec::new();
for batch in reader {
let batch = batch.map_err(|e| anyhow!("fsmeta section read: {e}"))?;
let paths = batch
.column_by_name("relative_path")
.and_then(|c| c.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| anyhow!("fsmeta section: missing/!Utf8 relative_path"))?;
let kinds = batch
.column_by_name("kind")
.and_then(|c| c.as_any().downcast_ref::<UInt8Array>())
.ok_or_else(|| anyhow!("fsmeta section: missing/!UInt8 kind"))?;
let modes = batch
.column_by_name("mode")
.and_then(|c| c.as_any().downcast_ref::<UInt32Array>())
.ok_or_else(|| anyhow!("fsmeta section: missing/!UInt32 mode"))?;
out.reserve(batch.num_rows());
for i in 0..batch.num_rows() {
let Some(kind) = FsKind::from_u8(kinds.value(i)) else { continue };
out.push(FsEntry {
relative_path: paths.value(i).to_string(),
kind,
mode: modes.value(i),
});
}
}
Ok(out)
}
pub fn read_fsmeta(archive: &Path) -> Result<Option<Vec<FsEntry>>> {
match read_reserved_section_bytes(archive, ZNIPPY_FSMETA_MODULE)? {
None => Ok(None),
Some(bytes) => Ok(Some(decode_fsmeta_section(&bytes)?)),
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct FsRestoreReport {
pub dirs_created: u64,
pub modes_applied: u64,
}
pub fn create_recorded_dirs(out_dir: &Path, entries: &[FsEntry]) -> Result<u64> {
let mut created = 0u64;
for e in entries.iter().filter(|e| e.kind == FsKind::Dir) {
let full = crate::decompress::safe_output_path(out_dir, &e.relative_path)?;
if !full.is_dir() {
std::fs::create_dir_all(&full)?;
created += 1;
}
}
Ok(created)
}
#[cfg(unix)]
pub fn apply_recorded_modes<'p, I>(out_dir: &Path, entries: &[FsEntry], extracted: I) -> Result<u64>
where
I: IntoIterator<Item = &'p str>,
{
use std::collections::HashSet;
use std::os::unix::fs::PermissionsExt;
let mut applied = 0u64;
let set = |rel: &str, mode: u32| -> Result<bool> {
let full = crate::decompress::safe_output_path(out_dir, rel)?;
if std::fs::symlink_metadata(&full).is_err() {
return Ok(false);
}
std::fs::set_permissions(&full, std::fs::Permissions::from_mode(mode))?;
Ok(true)
};
let mut explicit: HashSet<&str> = HashSet::new();
for e in entries.iter().filter(|e| e.kind == FsKind::File && e.mode != 0) {
explicit.insert(e.relative_path.as_str());
if set(&e.relative_path, e.mode)? {
applied += 1;
}
}
if let Some(def) = entries
.iter()
.find(|e| e.kind == FsKind::FileDefaultMode && e.mode != 0)
.map(|e| e.mode)
{
for rel in extracted {
if explicit.contains(rel) {
continue;
}
if set(rel, def)? {
applied += 1;
}
}
}
let mut dirs: Vec<&FsEntry> =
entries.iter().filter(|e| e.kind == FsKind::Dir && e.mode != 0).collect();
dirs.sort_by_key(|e| std::cmp::Reverse(e.relative_path.matches('/').count()));
for e in dirs {
if set(&e.relative_path, e.mode)? {
applied += 1;
}
}
Ok(applied)
}
#[cfg(not(unix))]
pub fn apply_recorded_modes<'p, I>(
_out_dir: &Path,
_entries: &[FsEntry],
_extracted: I,
) -> Result<u64>
where
I: IntoIterator<Item = &'p str>,
{
Ok(0)
}
#[cfg(unix)]
pub fn file_mode(meta: &std::fs::Metadata) -> Option<u32> {
use std::os::unix::fs::MetadataExt;
Some(meta.mode() & 0o7777)
}
#[cfg(not(unix))]
pub fn file_mode(_meta: &std::fs::Metadata) -> Option<u32> {
None
}
pub fn summarise_file_modes(modes: &[(String, u32)]) -> Vec<FsEntry> {
if modes.is_empty() {
return Vec::new();
}
let mut counts: HashMap<u32, usize> = HashMap::new();
for (_, m) in modes {
*counts.entry(*m).or_default() += 1;
}
let default = counts
.iter()
.max_by_key(|(mode, n)| (**n, std::cmp::Reverse(**mode)))
.map(|(mode, _)| *mode)
.unwrap_or(0);
let mut out = Vec::with_capacity(1 + modes.len() - counts[&default]);
out.push(FsEntry {
relative_path: String::new(),
kind: FsKind::FileDefaultMode,
mode: default,
});
for (path, mode) in modes {
if *mode != default {
out.push(FsEntry {
relative_path: path.clone(),
kind: FsKind::File,
mode: *mode,
});
}
}
out
}
#[cfg(unix)]
pub fn dir_mode(meta: &std::fs::Metadata) -> u32 {
use std::os::unix::fs::MetadataExt;
meta.mode() & 0o7777
}
#[cfg(not(unix))]
pub fn dir_mode(_meta: &std::fs::Metadata) -> u32 {
0
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Vec<FsEntry> {
vec![
FsEntry { relative_path: "blob".into(), kind: FsKind::Dir, mode: 0o755 },
FsEntry { relative_path: "empty-dir".into(), kind: FsKind::Dir, mode: 0o755 },
FsEntry { relative_path: "blob/run.sh".into(), kind: FsKind::File, mode: 0o755 },
FsEntry {
relative_path: "manifests/registry.k8s.io:5000/x@v1/ref/v1.30+b1".into(),
kind: FsKind::Dir,
mode: 0o700,
},
FsEntry { relative_path: "manifests/ünïcödé".into(), kind: FsKind::Dir, mode: 0o755 },
]
}
#[test]
fn shape_rows_round_trip_through_arrow() {
let batch = build_fsmeta_batch(&sample()).unwrap();
let mut bytes = Vec::new();
{
let mut w =
arrow::ipc::writer::StreamWriter::try_new(&mut bytes, &fsmeta_schema()).unwrap();
w.write(&batch).unwrap();
w.finish().unwrap();
}
let back = decode_fsmeta_section(&bytes).unwrap();
let mut want = sample();
want.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));
assert_eq!(back, want, "every row, in the deterministic sorted order");
}
#[test]
fn the_section_is_independent_of_walk_order() {
let a = build_fsmeta_batch(&sample()).unwrap();
let mut shuffled = sample();
shuffled.reverse();
let b = build_fsmeta_batch(&shuffled).unwrap();
assert_eq!(a, b, "sorted by path, so walk order cannot leak into the archive");
}
#[test]
fn an_unknown_kind_is_skipped_not_an_error() {
let schema = fsmeta_schema();
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(StringArray::from(vec!["a", "future/thing"])),
Arc::new(UInt8Array::from(vec![FsKind::Dir as u8, 200])),
Arc::new(UInt32Array::from(vec![0o755u32, 0o777])),
],
)
.unwrap();
let mut bytes = Vec::new();
{
let mut w = arrow::ipc::writer::StreamWriter::try_new(&mut bytes, &schema).unwrap();
w.write(&batch).unwrap();
w.finish().unwrap();
}
let back = decode_fsmeta_section(&bytes).unwrap();
assert_eq!(back.len(), 1, "the known row survives");
assert_eq!(back[0].relative_path, "a");
}
#[test]
fn a_traversing_dir_row_is_refused() {
let out = tempfile::tempdir().unwrap();
let rows = vec![FsEntry {
relative_path: "../escaped".into(),
kind: FsKind::Dir,
mode: 0o755,
}];
assert!(create_recorded_dirs(out.path(), &rows).is_err());
assert!(!out.path().parent().unwrap().join("escaped").exists());
}
#[cfg(unix)]
#[test]
fn a_read_only_directory_does_not_block_its_children() {
use std::os::unix::fs::PermissionsExt;
let out = tempfile::tempdir().unwrap();
let rows = vec![
FsEntry { relative_path: "locked".into(), kind: FsKind::Dir, mode: 0o555 },
FsEntry { relative_path: "locked/inner".into(), kind: FsKind::Dir, mode: 0o755 },
FsEntry { relative_path: "locked/inner/x".into(), kind: FsKind::File, mode: 0o750 },
];
create_recorded_dirs(out.path(), &rows).unwrap();
std::fs::write(out.path().join("locked/inner/x"), b"payload").unwrap();
apply_recorded_modes(out.path(), &rows, std::iter::empty()).unwrap();
let m = |p: &str| {
std::fs::metadata(out.path().join(p)).unwrap().permissions().mode() & 0o7777
};
assert_eq!(m("locked/inner/x"), 0o750);
assert_eq!(m("locked/inner"), 0o755);
assert_eq!(m("locked"), 0o555);
std::fs::set_permissions(out.path().join("locked"), std::fs::Permissions::from_mode(0o755))
.unwrap();
}
#[test]
fn the_bulk_mode_is_described_once_and_only_deviations_are_named() {
let modes: Vec<(String, u32)> = (0..40)
.map(|i| (format!("blob/{i:064x}"), 0o664))
.chain([("blob/run.sh".to_string(), 0o775), ("blob/ro".to_string(), 0o444)])
.collect();
let rows = summarise_file_modes(&modes);
assert_eq!(rows.len(), 3, "one default + two deviations, not 42 rows: {rows:?}");
assert_eq!(rows[0].kind, FsKind::FileDefaultMode);
assert_eq!(rows[0].mode, 0o664, "the MODAL mode, whatever the umask was");
assert!(rows[0].relative_path.is_empty(), "the default row is archive-scoped");
let named: Vec<_> = rows[1..].iter().map(|r| (r.relative_path.as_str(), r.mode)).collect();
assert_eq!(named, vec![("blob/run.sh", 0o775), ("blob/ro", 0o444)]);
}
#[cfg(unix)]
#[test]
fn the_default_reaches_unnamed_files_and_never_overrides_a_named_one() {
use std::os::unix::fs::PermissionsExt;
let out = tempfile::tempdir().unwrap();
for f in ["plain", "other", "run.sh"] {
std::fs::write(out.path().join(f), b"x").unwrap();
std::fs::set_permissions(out.path().join(f), std::fs::Permissions::from_mode(0o600))
.unwrap();
}
let rows = summarise_file_modes(&[
("plain".into(), 0o664),
("other".into(), 0o664),
("run.sh".into(), 0o775),
]);
let extracted = ["plain", "other", "run.sh"];
let n = apply_recorded_modes(out.path(), &rows, extracted).unwrap();
assert_eq!(n, 3, "all three files got a mode");
let m = |p: &str| {
std::fs::metadata(out.path().join(p)).unwrap().permissions().mode() & 0o7777
};
assert_eq!(m("plain"), 0o664, "the default reached it");
assert_eq!(m("other"), 0o664);
assert_eq!(m("run.sh"), 0o775, "the named row won over the default");
}
}