Skip to content

Commit 80989fd

Browse files
authored
Add filepack server subcommand (#82)
1 parent 07a6d28 commit 80989fd

File tree

21 files changed

+1882
-91
lines changed

21 files changed

+1882
-91
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ repository = "https://github.com/casey/filepack"
1313
include = ["CHANGELOG.md", "CONTRIBUTING", "LICENSE", "README.md", "src", "tests"]
1414

1515
[dependencies]
16+
axum = "0.8.4"
1617
blake3 = { version = "1.5.4", features = ["mmap", "rayon", "serde"] }
17-
boilerplate = "1.0.1"
18+
boilerplate = { version = "1.0.1", features = ["axum"] }
1819
camino = { version = "1.1.9", features = ["serde1"] }
1920
clap = { version = "4.5.16", features = ["derive"] }
2021
clap_mangen = "0.2.23"
@@ -31,14 +32,18 @@ serde_json = "1.0.127"
3132
serde_with = "3.11.0"
3233
serde_yaml = "0.9.34"
3334
snafu = "0.8.4"
35+
tokio = { version = "1.46.1", features = ["rt-multi-thread"] }
3436
walkdir = "2.5.0"
3537

3638
[dev-dependencies]
3739
assert_cmd = { version = "2.0.16", features = ["color-auto"] }
3840
assert_fs = { version = "1.1.2", features = ["color-auto"] }
41+
axum-test = "17.3.0"
42+
executable-path = "1.0.0"
3943
predicates = "3.1.2"
4044
pretty_assertions = "1.4.1"
4145
regex = "1.10.6"
46+
reqwest = { version = "0.12.22", features = ["blocking"] }
4247

4348
[lints.clippy]
4449
all = { level = "deny", priority = -1 }
@@ -49,6 +54,7 @@ needless-pass-by-value = "allow"
4954
pedantic = { level = "deny", priority = -1 }
5055
result-large-err = "allow"
5156
too-many-lines = "allow"
57+
unused-async = "allow"
5258
wildcard-imports = "allow"
5359

5460
[profile.release]

clippy.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
source-item-ordering = ['enum', 'struct', 'trait']
1+
source-item-ordering = ['enum', 'impl', 'struct', 'trait']

src/archive.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use super::*;
2+
3+
#[derive(Clone, Debug, PartialEq)]
4+
pub(crate) struct Archive {
5+
pub(crate) manifest: Hash,
6+
}
7+
8+
impl Archive {
9+
pub(crate) const EXTENSION: &str = "filepack";
10+
pub(crate) const FILE_SIGNATURE: &[u8] = b"FILEPACK";
11+
12+
pub(crate) fn load(path: &Utf8Path) -> Result<Self, ArchiveError> {
13+
let mut reader = BufReader::new(File::open(path).context(archive_error::FilesystemIo)?);
14+
15+
let mut signature = [0u8; Self::FILE_SIGNATURE.len()];
16+
17+
match reader.read_exact(&mut signature) {
18+
Ok(()) => {}
19+
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
20+
return Err(archive_error::FileSignature.build());
21+
}
22+
Err(error) => {
23+
return Err(archive_error::FilesystemIo.into_error(error));
24+
}
25+
}
26+
27+
if signature != Self::FILE_SIGNATURE {
28+
return Err(archive_error::FileSignature.build());
29+
}
30+
31+
let mut buffer = [0u8; Hash::LEN];
32+
33+
match reader.read_exact(&mut buffer) {
34+
Ok(()) => {}
35+
Err(error) if error.kind() == io::ErrorKind::UnexpectedEof => {
36+
return Err(archive_error::Truncated.build());
37+
}
38+
Err(error) => {
39+
return Err(archive_error::FilesystemIo.into_error(error));
40+
}
41+
}
42+
43+
Ok(Self {
44+
manifest: buffer.into(),
45+
})
46+
}
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
53+
#[test]
54+
fn file_signature_truncated_error() {
55+
let tempdir = TempDir::new().unwrap();
56+
57+
tempdir.child("foo.archive").write_binary(b"foo").unwrap();
58+
59+
let path = decode_path(tempdir.path()).unwrap();
60+
61+
assert_matches! {
62+
Archive::load(&path.join("foo.archive")),
63+
Err(ArchiveError::FileSignature { .. }),
64+
}
65+
}
66+
67+
#[test]
68+
fn file_signature_mismatch_error() {
69+
let tempdir = TempDir::new().unwrap();
70+
71+
tempdir
72+
.child("foo.archive")
73+
.write_binary(b"aaaaaaaa")
74+
.unwrap();
75+
76+
let path = decode_path(tempdir.path()).unwrap();
77+
78+
assert_matches! {
79+
Archive::load(&path.join("foo.archive")),
80+
Err(ArchiveError::FileSignature { .. }),
81+
}
82+
}
83+
84+
#[test]
85+
fn truncated_error() {
86+
let tempdir = TempDir::new().unwrap();
87+
88+
tempdir
89+
.child("foo.archive")
90+
.write_binary(b"FILEPACK")
91+
.unwrap();
92+
93+
let path = decode_path(tempdir.path()).unwrap();
94+
95+
assert_matches! {
96+
Archive::load(&path.join("foo.archive")),
97+
Err(ArchiveError::Truncated { .. }),
98+
}
99+
}
100+
101+
#[test]
102+
fn success() {
103+
let tempdir = TempDir::new().unwrap();
104+
105+
let manifest = Hash::bytes(&[]);
106+
107+
let archive = Archive::FILE_SIGNATURE
108+
.iter()
109+
.chain(manifest.as_bytes())
110+
.copied()
111+
.collect::<Vec<u8>>();
112+
113+
tempdir.child("foo.archive").write_binary(&archive).unwrap();
114+
115+
let path = decode_path(tempdir.path()).unwrap();
116+
117+
let archive = Archive::load(&path.join("foo.archive")).unwrap();
118+
119+
assert_eq!(archive, Archive { manifest });
120+
}
121+
}

src/archive_error.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
use super::*;
2+
3+
#[derive(Debug, Snafu)]
4+
#[snafu(context(suffix(false)), visibility(pub(crate)))]
5+
pub(crate) enum ArchiveError {
6+
#[snafu(display("invalid file signature"))]
7+
FileSignature { backtrace: Option<Backtrace> },
8+
#[snafu(display("I/O error"))]
9+
FilesystemIo {
10+
backtrace: Option<Backtrace>,
11+
source: io::Error,
12+
},
13+
#[snafu(display("archive truncated"))]
14+
Truncated { backtrace: Option<Backtrace> },
15+
}

src/error.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@ use super::*;
33
#[derive(Debug, Snafu)]
44
#[snafu(context(suffix(false)), visibility(pub(crate)))]
55
pub(crate) enum Error {
6+
#[snafu(display("failed to load archive at `{path}`"))]
7+
ArchiveLoad {
8+
path: DisplayPath,
9+
source: ArchiveError,
10+
},
611
#[snafu(display("failed to get current directory"))]
712
CurrentDir { source: io::Error },
813
#[snafu(display("failed to get local data directory"))]
@@ -43,6 +48,12 @@ pub(crate) enum Error {
4348
backtrace: Option<Backtrace>,
4449
path: DisplayPath,
4550
},
51+
#[snafu(display("I/O error at `{path}`"))]
52+
FilesystemIo {
53+
backtrace: Option<Backtrace>,
54+
path: DisplayPath,
55+
source: io::Error,
56+
},
4657
#[snafu(display("fingerprint mismatch"))]
4758
FingerprintMismatch { backtrace: Option<Backtrace> },
4859
#[snafu(display("file `{path}` hash {actual} does not match manifest hash {expected}"))]
@@ -52,12 +63,6 @@ pub(crate) enum Error {
5263
expected: Hash,
5364
path: DisplayPath,
5465
},
55-
#[snafu(display("I/O error at `{path}`"))]
56-
Io {
57-
backtrace: Option<Backtrace>,
58-
path: DisplayPath,
59-
source: io::Error,
60-
},
6166
#[snafu(display("public key `{public_key}` doesn't match private key `{private_key}`"))]
6267
KeyMismatch {
6368
backtrace: Option<Backtrace>,
@@ -136,6 +141,23 @@ pub(crate) enum Error {
136141
backtrace: Option<Backtrace>,
137142
path: DisplayPath,
138143
},
144+
#[snafu(display("failed to bind server to `{address}:{port}`"))]
145+
ServerBind {
146+
address: String,
147+
backtrace: Option<Backtrace>,
148+
port: u16,
149+
source: io::Error,
150+
},
151+
#[snafu(display("failed to run server"))]
152+
ServerRun {
153+
backtrace: Option<Backtrace>,
154+
source: io::Error,
155+
},
156+
#[snafu(display("failed to initialize server runtime"))]
157+
ServerRuntime {
158+
backtrace: Option<Backtrace>,
159+
source: io::Error,
160+
},
139161
#[snafu(display("manifest has already been signed by public key `{public_key}`"))]
140162
SignatureAlreadyExists {
141163
backtrace: Option<Backtrace>,

src/filesystem.rs

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

33
pub(crate) fn create_dir_all(path: &Utf8Path) -> Result<()> {
4-
std::fs::create_dir_all(path).context(error::Io { path })
4+
std::fs::create_dir_all(path).context(error::FilesystemIo { path })
55
}
66

77
pub(crate) fn exists(path: &Utf8Path) -> Result<bool> {
8-
path.try_exists().context(error::Io { path })
8+
path.try_exists().context(error::FilesystemIo { path })
99
}
1010

1111
pub(crate) fn metadata(path: &Utf8Path) -> Result<std::fs::Metadata> {
12-
std::fs::metadata(path).context(error::Io { path })
12+
std::fs::metadata(path).context(error::FilesystemIo { path })
1313
}
1414

1515
pub(crate) fn read_to_string(path: impl AsRef<Utf8Path>) -> Result<String> {
16-
std::fs::read_to_string(path.as_ref()).context(error::Io {
16+
std::fs::read_to_string(path.as_ref()).context(error::FilesystemIo {
1717
path: path.as_ref(),
1818
})
1919
}
2020

2121
pub(crate) fn read_to_string_opt(path: &Utf8Path) -> Result<Option<String>> {
2222
match std::fs::read_to_string(path) {
2323
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
24-
result => result.map(Some).context(error::Io { path }),
24+
result => result.map(Some).context(error::FilesystemIo { path }),
2525
}
2626
}
2727

2828
pub(crate) fn write(path: &Utf8Path, contents: impl AsRef<[u8]>) -> Result {
29-
std::fs::write(path, contents).context(error::Io { path })
29+
std::fs::write(path, contents).context(error::FilesystemIo { path })
3030
}

src/hash.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::*;
44
pub(crate) struct Hash(blake3::Hash);
55

66
impl Hash {
7-
const LEN: usize = blake3::OUT_LEN;
7+
pub(crate) const LEN: usize = blake3::OUT_LEN;
88

99
pub(crate) fn as_bytes(&self) -> &[u8; Self::LEN] {
1010
self.0.as_bytes()
@@ -27,6 +27,12 @@ impl From<Hash> for [u8; Hash::LEN] {
2727
}
2828
}
2929

30+
impl From<[u8; Hash::LEN]> for Hash {
31+
fn from(bytes: [u8; Hash::LEN]) -> Self {
32+
Self(bytes.into())
33+
}
34+
}
35+
3036
impl FromStr for Hash {
3137
type Err = blake3::HexError;
3238

src/main.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
use {
22
self::{
3-
arguments::Arguments, bytes::Bytes, display_path::DisplayPath, display_secret::DisplaySecret,
4-
entry::Entry, error::Error, hash::Hash, into_u64::IntoU64, lint::Lint, lint_group::LintGroup,
5-
list::List, manifest::Manifest, metadata::Metadata, options::Options,
6-
owo_colorize_ext::OwoColorizeExt, page::Page, private_key::PrivateKey, public_key::PublicKey,
7-
relative_path::RelativePath, signature::Signature, signature_error::SignatureError,
8-
style::Style, subcommand::Subcommand, template::Template, utf8_path_ext::Utf8PathExt,
3+
archive::Archive, archive_error::ArchiveError, arguments::Arguments, bytes::Bytes,
4+
display_path::DisplayPath, display_secret::DisplaySecret, entry::Entry, error::Error,
5+
hash::Hash, into_u64::IntoU64, lint::Lint, lint_group::LintGroup, list::List,
6+
manifest::Manifest, metadata::Metadata, options::Options, owo_colorize_ext::OwoColorizeExt,
7+
page::Page, private_key::PrivateKey, public_key::PublicKey, relative_path::RelativePath,
8+
signature::Signature, signature_error::SignatureError, style::Style, subcommand::Subcommand,
9+
template::Template, utf8_path_ext::Utf8PathExt,
910
},
1011
blake3::Hasher,
1112
boilerplate::Boilerplate,
@@ -17,7 +18,7 @@ use {
1718
owo_colors::Styled,
1819
serde::{Deserialize, Deserializer, Serialize, Serializer},
1920
serde_with::{DeserializeFromStr, SerializeDisplay},
20-
snafu::{ensure, ErrorCompat, OptionExt, ResultExt, Snafu},
21+
snafu::{ensure, ErrorCompat, IntoError, OptionExt, ResultExt, Snafu},
2122
std::{
2223
array::TryFromSliceError,
2324
backtrace::{Backtrace, BacktraceStatus},
@@ -26,17 +27,38 @@ use {
2627
env,
2728
fmt::{self, Display, Formatter},
2829
fs::{self, File},
29-
io::{self, BufWriter, IsTerminal, Write},
30+
io::{self, BufReader, BufWriter, IsTerminal, Read, Write},
3031
path::{Path, PathBuf},
3132
process,
3233
str::{self, FromStr},
34+
sync::Arc,
3335
},
36+
tokio::runtime::Runtime,
3437
walkdir::WalkDir,
3538
};
3639

3740
#[cfg(test)]
38-
use assert_fs::TempDir;
41+
use assert_fs::{
42+
fixture::{FileWriteBin, PathChild},
43+
TempDir,
44+
};
45+
46+
#[cfg(test)]
47+
macro_rules! assert_matches {
48+
($expression:expr, $( $pattern:pat_param )|+ $( if $guard:expr )? $(,)?) => {
49+
match $expression {
50+
$( $pattern )|+ $( if $guard )? => {}
51+
left => panic!(
52+
"assertion failed: (left ~= right)\n left: `{:?}`\n right: `{}`",
53+
left,
54+
stringify!($($pattern)|+ $(if $guard)?)
55+
),
56+
}
57+
}
58+
}
3959

60+
mod archive;
61+
mod archive_error;
4062
mod arguments;
4163
mod bytes;
4264
mod display_path;
@@ -75,6 +97,10 @@ fn current_dir() -> Result<Utf8PathBuf> {
7597
.map_err(|path| error::PathUnicode { path }.build())
7698
}
7799

100+
fn decode_path(path: &Path) -> Result<&Utf8Path> {
101+
Utf8Path::from_path(path).context(error::PathUnicode { path })
102+
}
103+
78104
fn main() {
79105
if let Err(err) = Arguments::parse().run() {
80106
let style = Style::stderr();

src/options.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,7 @@ impl Options {
3737
path.into()
3838
} else {
3939
let path = dirs::data_local_dir().context(error::DataLocalDir)?;
40-
Utf8Path::from_path(&path)
41-
.context(error::PathUnicode { path: &path })?
42-
.join("filepack")
40+
decode_path(&path)?.join("filepack")
4341
};
4442

4543
Ok(path.join("keys"))

0 commit comments

Comments
 (0)