use crate::error::{Error, Result};
use crate::tlv::types::FOLDER_INDEX;
use std::collections::BTreeMap;
const fn bad(reason: &'static str) -> Error {
Error::MalformedTlv {
tlv_type: FOLDER_INDEX,
reason,
}
}
pub const INDEX_VERSION: u16 = 1;
pub const RECOMMENDED_MAX_ENTRIES: usize = 50_000;
pub const MAX_COMPONENT_LEN: usize = 255;
pub const MAX_INDEX_SIZE: usize = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryKind {
File,
Dir,
Symlink,
}
impl EntryKind {
const fn as_u8(self) -> u8 {
match self {
Self::File => 0,
Self::Dir => 1,
Self::Symlink => 2,
}
}
const fn from_u8(v: u8) -> Option<Self> {
match v {
0 => Some(Self::File),
1 => Some(Self::Dir),
2 => Some(Self::Symlink),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct EntryMeta {
pub mtime_ns: Option<i128>,
pub btime_ns: Option<i128>,
pub mode: Option<u32>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub xattrs: BTreeMap<String, Vec<u8>>,
}
impl EntryMeta {
#[must_use]
pub fn is_empty(&self) -> bool {
self.mtime_ns.is_none()
&& self.btime_ns.is_none()
&& self.mode.is_none()
&& self.uid.is_none()
&& self.gid.is_none()
&& self.xattrs.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContainerEntry {
pub path: Vec<String>,
pub kind: EntryKind,
pub offset: u64,
pub size: u64,
pub hash: Option<[u8; 32]>,
pub target: Option<String>,
pub meta: EntryMeta,
}
impl ContainerEntry {
#[must_use]
pub fn file(path: Vec<String>, offset: u64, size: u64) -> Self {
Self {
path,
kind: EntryKind::File,
offset,
size,
hash: None,
target: None,
meta: EntryMeta::default(),
}
}
#[must_use]
pub fn dir(path: Vec<String>) -> Self {
Self {
path,
kind: EntryKind::Dir,
offset: 0,
size: 0,
hash: None,
target: None,
meta: EntryMeta::default(),
}
}
#[must_use]
pub fn symlink(path: Vec<String>, target: String) -> Self {
Self {
path,
kind: EntryKind::Symlink,
offset: 0,
size: 0,
hash: None,
target: Some(target),
meta: EntryMeta::default(),
}
}
#[must_use]
pub fn display_path(&self) -> String {
self.path.join("/")
}
#[must_use]
pub fn range(&self) -> Option<(u64, u64)> {
if self.kind == EntryKind::File {
Some((self.offset, self.size))
} else {
None
}
}
}
pub fn validate_component(c: &str) -> Result<()> {
if c.is_empty() {
return Err(bad("路径组件为空"));
}
if c == "." || c == ".." {
return Err(bad("路径组件为 . 或 ..,可能造成目录穿越"));
}
if c.len() > MAX_COMPONENT_LEN {
return Err(bad("路径组件超过 255 字节"));
}
if c.contains('/') || c.contains('\\') {
return Err(bad("路径组件含分隔符"));
}
if c.contains('\0') {
return Err(bad("路径组件含 NUL 字节"));
}
{
let mut ch = c.chars();
if let (Some(a), Some(b)) = (ch.next(), ch.next())
&& a.is_ascii_alphabetic()
&& b == ':'
{
return Err(bad("路径组件形如盘符"));
}
}
if c.contains(':') {
return Err(bad("路径组件含冒号"));
}
Ok(())
}
fn truncate_utf8(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end = end.saturating_sub(1);
}
s.get(..end).unwrap_or("")
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContainerIndex {
pub root: String,
pub entries: Vec<ContainerEntry>,
}
impl ContainerIndex {
#[must_use]
pub fn new(root: impl Into<String>) -> Self {
Self {
root: root.into(),
entries: Vec::new(),
}
}
#[must_use]
pub fn total_size(&self) -> u64 {
self.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.map(|e| e.size)
.sum()
}
#[must_use]
pub fn file_count(&self) -> usize {
self.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.count()
}
#[must_use]
pub fn find(&self, display_path: &str) -> Option<&ContainerEntry> {
self.entries.iter().find(|e| e.display_path() == display_path)
}
#[must_use]
pub fn encode(&self) -> Vec<u8> {
let root_str = truncate_utf8(&self.root, u16::MAX as usize);
let root = root_str.as_bytes();
let mut out = Vec::with_capacity(64usize.saturating_add(self.entries.len().saturating_mul(48)));
out.extend_from_slice(&INDEX_VERSION.to_le_bytes());
let root_len = u16::try_from(root.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&root_len.to_le_bytes());
let count = u32::try_from(self.entries.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&count.to_le_bytes());
out.extend_from_slice(root);
for e in &self.entries {
let mut flags = 0u8;
if e.hash.is_some() {
flags |= 1 << 0;
}
if e.target.is_some() {
flags |= 1 << 1;
}
if !e.meta.is_empty() {
flags |= 1 << 2;
}
out.push(e.kind.as_u8());
out.push(flags);
let ncomp = u16::try_from(e.path.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&ncomp.to_le_bytes());
out.extend_from_slice(&e.offset.to_le_bytes());
out.extend_from_slice(&e.size.to_le_bytes());
for c in e.path.iter().take(ncomp as usize) {
let cs = truncate_utf8(c, u16::MAX as usize);
let b = cs.as_bytes();
let l = u16::try_from(b.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&l.to_le_bytes());
out.extend_from_slice(b);
}
if let Some(h) = &e.hash {
out.extend_from_slice(h);
}
if let Some(t) = &e.target {
let ts = truncate_utf8(t, u16::MAX as usize);
let b = ts.as_bytes();
let l = u16::try_from(b.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&l.to_le_bytes());
out.extend_from_slice(b);
}
if !e.meta.is_empty() {
encode_meta(&e.meta, &mut out);
}
}
out
}
pub fn parse(data: &[u8]) -> Result<Self> {
if data.len() > MAX_INDEX_SIZE {
return Err(bad("索引超过 64 MiB 上限"));
}
let mut p = Cursor::new(data);
let version = p.u16()?;
if version != INDEX_VERSION {
return Err(bad("索引版本不支持"));
}
let root_len = p.u16()? as usize;
let count = p.u32()? as usize;
let remaining = data.len().saturating_sub(8).saturating_sub(root_len);
if count.saturating_mul(20) > remaining {
return Err(bad("条目数与数据长度不符"));
}
let root = String::from_utf8(p.take(root_len)?.to_vec())
.map_err(|_| bad("root 不是合法 UTF-8"))?;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let kind = EntryKind::from_u8(p.u8()?)
.ok_or(bad("未知的条目类型"))?;
let flags = p.u8()?;
let ncomp = p.u16()? as usize;
let offset = p.u64()?;
let size = p.u64()?;
if ncomp == 0 {
return Err(bad("条目路径为空"));
}
let mut path = Vec::with_capacity(ncomp.min(64));
for _ in 0..ncomp {
let l = p.u16()? as usize;
let c = String::from_utf8(p.take(l)?.to_vec())
.map_err(|_| bad("路径组件不是合法 UTF-8"))?;
validate_component(&c)?;
path.push(c);
}
let hash = if flags & (1 << 0) != 0 {
let h: [u8; 32] = p
.take(32)?
.try_into()
.map_err(|_| bad("hash 长度不符"))?;
Some(h)
} else {
None
};
let target = if flags & (1 << 1) != 0 {
let l = p.u16()? as usize;
Some(
String::from_utf8(p.take(l)?.to_vec())
.map_err(|_| bad("链接目标不是合法 UTF-8"))?,
)
} else {
None
};
let meta = if flags & (1 << 2) != 0 {
decode_meta(&mut p)?
} else {
EntryMeta::default()
};
if kind != EntryKind::File && (offset != 0 || size != 0) {
return Err(bad("非文件条目不应声明载荷区间"));
}
if kind == EntryKind::Symlink && target.is_none() {
return Err(bad("符号链接缺少目标"));
}
if kind == EntryKind::File && offset.checked_add(size).is_none() {
return Err(bad("载荷区间溢出"));
}
entries.push(ContainerEntry {
path,
kind,
offset,
size,
hash,
target,
meta,
});
}
let idx = Self { root, entries };
idx.validate_layout()?;
Ok(idx)
}
pub fn validate_layout(&self) -> Result<()> {
let mut files: Vec<&ContainerEntry> = self
.entries
.iter()
.filter(|e| e.kind == EntryKind::File)
.collect();
files.sort_by_key(|e| e.offset);
let mut expect = 0u64;
for f in files {
if f.offset != expect {
return Err(bad("文件区间不连续或存在重叠"));
}
expect = expect
.checked_add(f.size)
.ok_or(bad("载荷区间溢出"))?;
}
let mut seen = std::collections::HashSet::with_capacity(self.entries.len());
for e in &self.entries {
if !seen.insert(e.display_path()) {
return Err(bad("存在重复路径"));
}
}
Ok(())
}
}
struct Cursor<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
const fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn take(&mut self, n: usize) -> Result<&'a [u8]> {
let end = self
.pos
.checked_add(n)
.ok_or(bad("索引偏移溢出"))?;
let s = self
.data
.get(self.pos..end)
.ok_or(bad("索引数据意外结束"))?;
self.pos = end;
Ok(s)
}
fn u8(&mut self) -> Result<u8> {
self.take(1)?.first().copied().ok_or(bad("读取 u8 失败"))
}
fn u16(&mut self) -> Result<u16> {
let b: [u8; 2] = self
.take(2)?
.try_into()
.map_err(|_| bad("读取 u16 失败"))?;
Ok(u16::from_le_bytes(b))
}
fn u32(&mut self) -> Result<u32> {
let b: [u8; 4] = self
.take(4)?
.try_into()
.map_err(|_| bad("读取 u32 失败"))?;
Ok(u32::from_le_bytes(b))
}
fn u64(&mut self) -> Result<u64> {
let b: [u8; 8] = self
.take(8)?
.try_into()
.map_err(|_| bad("读取 u64 失败"))?;
Ok(u64::from_le_bytes(b))
}
}
fn encode_meta(m: &EntryMeta, out: &mut Vec<u8>) {
let mut flags = 0u8;
if m.mtime_ns.is_some() {
flags |= 1 << 0;
}
if m.btime_ns.is_some() {
flags |= 1 << 1;
}
if m.mode.is_some() {
flags |= 1 << 2;
}
if m.uid.is_some() || m.gid.is_some() {
flags |= 1 << 3;
}
if !m.xattrs.is_empty() {
flags |= 1 << 4;
}
out.push(flags);
if let Some(t) = m.mtime_ns {
out.extend_from_slice(&t.to_le_bytes());
}
if let Some(t) = m.btime_ns {
out.extend_from_slice(&t.to_le_bytes());
}
if let Some(v) = m.mode {
out.extend_from_slice(&v.to_le_bytes());
}
if m.uid.is_some() || m.gid.is_some() {
out.extend_from_slice(&m.uid.unwrap_or(0).to_le_bytes());
out.extend_from_slice(&m.gid.unwrap_or(0).to_le_bytes());
}
if !m.xattrs.is_empty() {
let n = u16::try_from(m.xattrs.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&n.to_le_bytes());
for (k, v) in m.xattrs.iter().take(n as usize) {
let ks = truncate_utf8(k, u16::MAX as usize);
let kb = ks.as_bytes();
let kl = u16::try_from(kb.len()).unwrap_or(u16::MAX);
out.extend_from_slice(&kl.to_le_bytes());
out.extend_from_slice(kb);
let vl = u32::try_from(v.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&vl.to_le_bytes());
let vend = (vl as usize).min(v.len());
out.extend_from_slice(v.get(..vend).unwrap_or(&[]));
}
}
}
fn decode_meta(p: &mut Cursor<'_>) -> Result<EntryMeta> {
let flags = p.u8()?;
let mut m = EntryMeta::default();
if flags & (1 << 0) != 0 {
let b: [u8; 16] = p
.take(16)?
.try_into()
.map_err(|_| bad("mtime 长度不符"))?;
m.mtime_ns = Some(i128::from_le_bytes(b));
}
if flags & (1 << 1) != 0 {
let b: [u8; 16] = p
.take(16)?
.try_into()
.map_err(|_| bad("btime 长度不符"))?;
m.btime_ns = Some(i128::from_le_bytes(b));
}
if flags & (1 << 2) != 0 {
m.mode = Some(p.u32()?);
}
if flags & (1 << 3) != 0 {
m.uid = Some(p.u32()?);
m.gid = Some(p.u32()?);
}
if flags & (1 << 4) != 0 {
let n = p.u16()? as usize;
for _ in 0..n {
let kl = p.u16()? as usize;
let k = String::from_utf8(p.take(kl)?.to_vec())
.map_err(|_| bad("xattr 键不是合法 UTF-8"))?;
let vl = p.u32()? as usize;
if vl > MAX_INDEX_SIZE {
return Err(bad("xattr 值过大"));
}
let v = p.take(vl)?.to_vec();
m.xattrs.insert(k, v);
}
}
Ok(m)
}
#[derive(Debug, Default)]
pub struct ContainerBuilder {
index: ContainerIndex,
cursor: u64,
}
impl ContainerBuilder {
#[must_use]
pub fn new(root: impl Into<String>) -> Self {
Self {
index: ContainerIndex::new(root),
cursor: 0,
}
}
pub fn add_file(
&mut self,
path: Vec<String>,
size: u64,
hash: Option<[u8; 32]>,
meta: EntryMeta,
) -> Result<()> {
for c in &path {
validate_component(c)?;
}
let offset = self.cursor;
self.cursor = self
.cursor
.checked_add(size)
.ok_or(bad("容器载荷总长溢出"))?;
self.index.entries.push(ContainerEntry {
path,
kind: EntryKind::File,
offset,
size,
hash,
target: None,
meta,
});
Ok(())
}
pub fn add_dir(&mut self, path: Vec<String>, meta: EntryMeta) -> Result<()> {
for c in &path {
validate_component(c)?;
}
let mut e = ContainerEntry::dir(path);
e.meta = meta;
self.index.entries.push(e);
Ok(())
}
pub fn add_symlink(
&mut self,
path: Vec<String>,
target: String,
meta: EntryMeta,
) -> Result<()> {
for c in &path {
validate_component(c)?;
}
let mut e = ContainerEntry::symlink(path, target);
e.meta = meta;
self.index.entries.push(e);
Ok(())
}
#[must_use]
pub const fn payload_len(&self) -> u64 {
self.cursor
}
pub fn finish(self) -> Result<ContainerIndex> {
self.index.validate_layout()?;
Ok(self.index)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_basic() {
let mut b = ContainerBuilder::new("工作资料");
b.add_dir(vec!["文档".into()], EntryMeta::default()).unwrap();
b.add_file(
vec!["文档".into(), "季度报告.docx".into()],
2451,
Some([7u8; 32]),
EntryMeta {
mtime_ns: Some(1_756_425_600_000_000_000),
mode: Some(0o100_644),
..EntryMeta::default()
},
)
.unwrap();
b.add_file(vec!["readme.txt".into()], 12, None, EntryMeta::default())
.unwrap();
b.add_symlink(
vec!["链接.txt".into()],
"文档/季度报告.docx".into(),
EntryMeta::default(),
)
.unwrap();
let idx = b.finish().unwrap();
assert_eq!(idx.file_count(), 2);
assert_eq!(idx.total_size(), 2463);
let enc = idx.encode();
let back = ContainerIndex::parse(&enc).unwrap();
assert_eq!(idx, back);
assert_eq!(back.root, "工作资料");
}
#[test]
fn offsets_are_contiguous() {
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["a".into()], 100, None, EntryMeta::default())
.unwrap();
b.add_file(vec!["b".into()], 250, None, EntryMeta::default())
.unwrap();
b.add_file(vec!["c".into()], 0, None, EntryMeta::default())
.unwrap();
let idx = b.finish().unwrap();
assert_eq!(idx.entries[0].range(), Some((0, 100)));
assert_eq!(idx.entries[1].range(), Some((100, 250)));
assert_eq!(idx.entries[2].range(), Some((350, 0)));
assert_eq!(idx.total_size(), 350);
}
#[test]
fn rejects_path_traversal() {
for bad in ["..", ".", "", "a/b", "a\\b", "C:", "a\0b"] {
assert!(
validate_component(bad).is_err(),
"组件 {bad:?} 本应被拒绝"
);
}
let mut b = ContainerBuilder::new("r");
assert!(
b.add_file(vec!["..".into(), "etc".into()], 1, None, EntryMeta::default())
.is_err()
);
}
#[test]
fn rejects_colon_in_any_position() {
for bad in [
"C:", "z:", "a.txt:stream", "中文:流", "报告.docx:evil", ":leading", "trailing:", ] {
assert!(
validate_component(bad).is_err(),
"组件 {bad:?} 含冒号,本应被拒绝"
);
}
assert!(validate_component("季度报告.docx").is_ok());
assert!(validate_component("a.txt").is_ok());
}
#[test]
fn utf8_truncation_never_panics() {
let s = "中文字符串测试";
for max in 0..=s.len() + 3 {
let t = truncate_utf8(s, max);
assert!(t.len() <= max.min(s.len()), "截断结果超长");
assert!(s.starts_with(t), "截断结果应是原串前缀");
}
assert_eq!(truncate_utf8("中", 1), "");
assert_eq!(truncate_utf8("中", 2), "");
assert_eq!(truncate_utf8("中", 3), "中");
assert_eq!(truncate_utf8("abcdef", 3), "abc");
assert_eq!(truncate_utf8("ab", 10), "ab");
}
#[test]
fn oversized_root_encodes_without_panic() {
let long_root = "工".repeat(30_000); assert!(long_root.len() > u16::MAX as usize);
let idx = ContainerIndex::new(long_root);
let enc = idx.encode(); let back = ContainerIndex::parse(&enc).unwrap();
assert!(back.root.len() <= u16::MAX as usize);
assert!(back.root.chars().all(|c| c == '工'), "截断处不应出现替换字符");
}
#[test]
fn oversized_xattr_value_encodes_without_panic() {
let mut b = ContainerBuilder::new("r");
b.add_file(
vec!["f".into()],
0,
None,
EntryMeta {
xattrs: [("键".repeat(100), vec![0u8; 10])].into_iter().collect(),
..EntryMeta::default()
},
)
.unwrap();
let enc = b.finish().unwrap().encode(); assert!(ContainerIndex::parse(&enc).is_ok());
}
#[test]
fn rejects_traversal_in_parsed_data() {
let mut idx = ContainerIndex::new("r");
idx.entries.push(ContainerEntry::file(vec!["ok".into()], 0, 5));
let mut enc = idx.encode();
let pos = enc.windows(2).position(|w| w == b"ok").unwrap();
enc[pos] = b'.';
enc[pos + 1] = b'.';
assert!(ContainerIndex::parse(&enc).is_err());
}
#[test]
fn rejects_overlapping_ranges() {
let mut idx = ContainerIndex::new("r");
idx.entries.push(ContainerEntry::file(vec!["a".into()], 0, 100));
idx.entries.push(ContainerEntry::file(vec!["b".into()], 50, 100));
assert!(idx.validate_layout().is_err());
}
#[test]
fn rejects_gap_between_ranges() {
let mut idx = ContainerIndex::new("r");
idx.entries.push(ContainerEntry::file(vec!["a".into()], 0, 100));
idx.entries.push(ContainerEntry::file(vec!["b".into()], 200, 10));
assert!(idx.validate_layout().is_err());
}
#[test]
fn rejects_duplicate_paths() {
let mut idx = ContainerIndex::new("r");
idx.entries.push(ContainerEntry::file(vec!["a".into()], 0, 10));
idx.entries.push(ContainerEntry::file(vec!["a".into()], 10, 10));
assert!(idx.validate_layout().is_err());
}
#[test]
fn rejects_dir_with_payload_range() {
let mut idx = ContainerIndex::new("r");
let mut d = ContainerEntry::dir(vec!["d".into()]);
d.size = 100; idx.entries.push(d);
let enc = idx.encode();
assert!(ContainerIndex::parse(&enc).is_err());
}
#[test]
fn truncated_input_never_panics() {
let mut b = ContainerBuilder::new("root");
b.add_file(vec!["f".into()], 10, Some([1u8; 32]), EntryMeta {
mtime_ns: Some(1),
mode: Some(2),
uid: Some(3),
gid: Some(4),
xattrs: [("k".to_string(), vec![9u8; 4])].into_iter().collect(),
btime_ns: Some(5),
})
.unwrap();
let enc = b.finish().unwrap().encode();
for n in 0..enc.len() {
let _ = ContainerIndex::parse(&enc[..n]);
}
}
#[test]
fn absurd_entry_count_rejected_without_huge_alloc() {
let mut data = Vec::new();
data.extend_from_slice(&1u16.to_le_bytes());
data.extend_from_slice(&0u16.to_le_bytes());
data.extend_from_slice(&u32::MAX.to_le_bytes());
assert!(ContainerIndex::parse(&data).is_err());
}
#[test]
fn meta_roundtrip_full() {
let meta = EntryMeta {
mtime_ns: Some(-12_345),
btime_ns: Some(i128::from(i64::MAX)),
mode: Some(0o777),
uid: Some(1000),
gid: Some(1000),
xattrs: [
("user.comment".to_string(), b"hello".to_vec()),
("user.empty".to_string(), Vec::new()),
]
.into_iter()
.collect(),
};
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["f".into()], 1, None, meta.clone()).unwrap();
let enc = b.finish().unwrap().encode();
let back = ContainerIndex::parse(&enc).unwrap();
assert_eq!(back.entries[0].meta, meta);
}
#[test]
fn empty_dir_is_preserved() {
let mut b = ContainerBuilder::new("r");
b.add_dir(vec!["空目录".into()], EntryMeta::default()).unwrap();
let idx = b.finish().unwrap();
assert_eq!(idx.entries.len(), 1);
assert_eq!(idx.file_count(), 0);
let back = ContainerIndex::parse(&idx.encode()).unwrap();
assert_eq!(back.entries[0].kind, EntryKind::Dir);
assert_eq!(back.entries[0].display_path(), "空目录");
}
#[test]
fn find_by_path() {
let mut b = ContainerBuilder::new("r");
b.add_file(vec!["dir".into(), "movie.mp4".into()], 5000, None, EntryMeta::default())
.unwrap();
let idx = b.finish().unwrap();
let e = idx.find("dir/movie.mp4").unwrap();
assert_eq!(e.range(), Some((0, 5000)));
assert!(idx.find("nope").is_none());
}
#[test]
fn symlink_without_target_rejected() {
let mut idx = ContainerIndex::new("r");
let mut s = ContainerEntry::symlink(vec!["l".into()], "t".into());
s.target = None; idx.entries.push(s);
let enc = idx.encode();
assert!(ContainerIndex::parse(&enc).is_err());
}
#[test]
fn long_component_rejected() {
let long = "a".repeat(MAX_COMPONENT_LEN + 1);
assert!(validate_component(&long).is_err());
let ok = "a".repeat(MAX_COMPONENT_LEN);
assert!(validate_component(&ok).is_ok());
}
}