Skip to content

Commit 120963f

Browse files
committed
Switch to hierarchical manifest format
1 parent 3db1195 commit 120963f

33 files changed

+1385
-1100
lines changed

Cargo.lock

Lines changed: 468 additions & 281 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,15 @@ serde_json = "1.0.127"
2929
serde_with = "3.11.0"
3030
serde_yaml = "0.9.34"
3131
snafu = "0.8.4"
32+
strum = { version = "0.27.2", features = ["derive"] }
33+
usized = "0.0.2"
3234
walkdir = "2.5.0"
3335

3436
[dev-dependencies]
3537
assert_cmd = { version = "2.0.16", features = ["color-auto"] }
3638
assert_fs = { version = "1.1.2", features = ["color-auto"] }
3739
predicates = "3.1.2"
40+
pretty_assertions = "1.4.1"
3841
regex = "1.10.6"
3942

4043
[lints.clippy]

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,9 +197,11 @@ An example manifest for a directory containing the files `README.md` and
197197
"hash": "5a9a6d96244ec398545fc0c98c2cb7ed52511b025c19e9ad1e3c1ef4ac8575ad",
198198
"size": 1573
199199
},
200-
"src/main.c": {
201-
"hash": "38abf296dc2a90f66f7870fe0ce584af3859668cf5140c7557a76786189dcf0f",
202-
"size": 4491
200+
"src": {
201+
"main.c": {
202+
"hash": "38abf296dc2a90f66f7870fe0ce584af3859668cf5140c7557a76786189dcf0f",
203+
"size": 4491
204+
}
203205
}
204206
}
205207
}

bin/forbid

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env bash
22

3-
set -euxo pipefail
3+
set -euo pipefail
44

55
which rg
66

src/component.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
use super::*;
2+
3+
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
4+
#[serde(deny_unknown_fields, rename_all = "kebab-case", transparent)]
5+
pub(crate) struct Component(String);
6+
7+
impl Component {
8+
pub(crate) fn as_bytes(&self) -> &[u8] {
9+
self.0.as_bytes()
10+
}
11+
12+
pub(crate) fn as_str(&self) -> &str {
13+
&self.0
14+
}
15+
}
16+
17+
impl Borrow<str> for Component {
18+
fn borrow(&self) -> &str {
19+
self.0.as_str()
20+
}
21+
}
22+
23+
impl FromStr for Component {
24+
type Err = PathError;
25+
26+
fn from_str(s: &str) -> Result<Self, Self::Err> {
27+
for character in s.chars() {
28+
if SEPARATORS.contains(&character) {
29+
return Err(PathError::Separator { character });
30+
}
31+
}
32+
33+
if s == ".." || s == "." {
34+
return Err(PathError::Component {
35+
component: s.into(),
36+
});
37+
}
38+
39+
if s.is_empty() {
40+
return Err(PathError::Empty);
41+
}
42+
43+
Ok(Self(s.into()))
44+
}
45+
}

src/context.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use super::*;
2+
3+
#[derive(Clone, Copy, IntoStaticStr)]
4+
#[strum(serialize_all = "kebab-case")]
5+
pub(crate) enum Context {
6+
Directory,
7+
Entry,
8+
File,
9+
}
10+
11+
impl Context {
12+
fn name(self) -> &'static str {
13+
self.into()
14+
}
15+
}
16+
17+
impl Display for Context {
18+
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
19+
write!(f, "{}", self.name())
20+
}
21+
}

src/directory.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
use super::*;
2+
3+
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
4+
#[serde(deny_unknown_fields, rename_all = "kebab-case", transparent)]
5+
pub struct Directory {
6+
pub(crate) entries: BTreeMap<Component, Entry>,
7+
}
8+
9+
impl Directory {
10+
pub(crate) fn create_directory(&mut self, path: &RelativePath) -> Result {
11+
let mut current = self;
12+
for component in path.components() {
13+
current = current.create_directory_entry(component)?;
14+
}
15+
Ok(())
16+
}
17+
18+
fn create_directory_entry(&mut self, component: Component) -> Result<&mut Directory> {
19+
let entry = self
20+
.entries
21+
.entry(component)
22+
.or_insert(Entry::Directory(Directory::new()));
23+
24+
match entry {
25+
Entry::Directory(directory) => Ok(directory),
26+
Entry::File(_file) => Err(
27+
error::Internal {
28+
message: "entry `{component}` already contains file",
29+
}
30+
.build(),
31+
),
32+
}
33+
}
34+
35+
pub(crate) fn create_file(&mut self, path: &RelativePath, file: File) -> Result {
36+
let mut components = path.components().peekable();
37+
38+
let mut current = self;
39+
while let Some(component) = components.next() {
40+
if components.peek().is_none() {
41+
ensure! {
42+
current.entries.insert(component, Entry::File(file)).is_none(),
43+
error::Internal {
44+
message: "entry `{component}` already contains file",
45+
}
46+
}
47+
return Ok(());
48+
}
49+
50+
current = current.create_directory_entry(component)?;
51+
}
52+
53+
Ok(())
54+
}
55+
56+
pub(crate) fn fingerprint(&self) -> Hash {
57+
let mut hasher = FieldHasher::new(Context::Directory);
58+
59+
hasher.array(0, self.entries.len().into_u64());
60+
61+
for (component, entry) in &self.entries {
62+
hasher.element(entry.fingerprint(component));
63+
}
64+
65+
hasher.finalize()
66+
}
67+
68+
pub(crate) fn is_empty(&self) -> bool {
69+
self.entries.is_empty()
70+
}
71+
72+
pub(crate) fn new() -> Self {
73+
Self::default()
74+
}
75+
}

src/entry.rs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
use super::*;
22

3-
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
4-
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
5-
pub struct Entry {
6-
pub hash: Hash,
7-
pub size: u64,
3+
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
4+
#[serde(deny_unknown_fields, rename_all = "kebab-case", untagged)]
5+
pub(crate) enum Entry {
6+
Directory(Directory),
7+
File(File),
8+
}
9+
10+
impl Entry {
11+
pub(crate) fn fingerprint(&self, component: &Component) -> Hash {
12+
let mut hasher = FieldHasher::new(Context::Entry);
13+
14+
hasher.field(0, component.as_bytes());
15+
16+
let inner = match self {
17+
Self::Directory(directory) => directory.fingerprint(),
18+
Self::File(file) => file.fingerprint(),
19+
};
20+
21+
hasher.field(1, inner.as_bytes());
22+
23+
hasher.finalize()
24+
}
825
}

src/error.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,6 @@ pub enum Error {
2525
path: DisplayPath,
2626
source: serde_yaml::Error,
2727
},
28-
#[snafu(
29-
display(
30-
"empty director{} {}",
31-
if paths.len() == 1 { "y" } else { "ies" },
32-
List::and_ticked(paths),
33-
)
34-
)]
35-
EmptyDirectory { paths: Vec<DisplayPath> },
3628
#[snafu(display("{count} mismatched file{}", if *count == 1 { "" } else { "s" }))]
3729
EntryMismatch {
3830
backtrace: Option<Backtrace>,
@@ -51,6 +43,11 @@ pub enum Error {
5143
},
5244
#[snafu(display("fingerprint mismatch"))]
5345
FingerprintMismatch { backtrace: Option<Backtrace> },
46+
#[snafu(display("internal error, this may indicate a bug in filepack: {message}"))]
47+
Internal {
48+
backtrace: Option<Backtrace>,
49+
message: String,
50+
},
5451
#[snafu(display("public key `{public_key}` doesn't match private key `{private_key}`"))]
5552
KeyMismatch {
5653
backtrace: Option<Backtrace>,
@@ -90,7 +87,7 @@ pub enum Error {
9087
#[snafu(display("invalid path `{path}`"))]
9188
Path {
9289
path: DisplayPath,
93-
source: relative_path::Error,
90+
source: PathError,
9491
},
9592
#[snafu(display("path not valid unicode: `{}`", path.display()))]
9693
PathUnicode {

src/field_hasher.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
use super::*;
2+
3+
pub(crate) struct FieldHasher {
4+
array: Option<NonZeroU64>,
5+
hasher: Hasher,
6+
next: u8,
7+
}
8+
9+
impl FieldHasher {
10+
pub(crate) fn array(&mut self, tag: u8, len: u64) {
11+
self.tag(tag);
12+
self.array = NonZeroU64::new(len);
13+
self.hasher.update(&len.to_le_bytes());
14+
}
15+
16+
pub(crate) fn element(&mut self, hash: Hash) {
17+
self.array = NonZeroU64::new(self.array.unwrap().get() - 1);
18+
self.hasher.update(hash.as_bytes());
19+
}
20+
21+
pub(crate) fn field(&mut self, tag: u8, contents: &[u8]) {
22+
self.tag(tag);
23+
self.hasher.update(&contents.len().into_u64().to_le_bytes());
24+
self.hasher.update(contents);
25+
}
26+
27+
pub(crate) fn finalize(self) -> Hash {
28+
assert_eq!(self.array, None);
29+
self.hasher.finalize().into()
30+
}
31+
32+
pub(crate) fn new(context: Context) -> Self {
33+
Self {
34+
array: None,
35+
hasher: Hasher::new_derive_key(&format!("filepack:{context}")),
36+
next: 0,
37+
}
38+
}
39+
40+
pub(crate) fn tag(&mut self, tag: u8) {
41+
assert_eq!(self.next, tag);
42+
assert_eq!(self.array, None);
43+
self.next = self.next.checked_add(1).unwrap();
44+
self.hasher.update(&[tag]);
45+
}
46+
}

0 commit comments

Comments
 (0)