Compare commits
3 commits
908e0031e2
...
b3bccbbf65
| Author | SHA1 | Date | |
|---|---|---|---|
| b3bccbbf65 | |||
| d4cc102a0f | |||
| 3257a97351 |
8 changed files with 176 additions and 82 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
|
@ -79,6 +79,15 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -108,6 +117,12 @@ version = "3.17.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.10.1"
|
||||
|
|
@ -874,6 +889,12 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
|
|
@ -944,6 +965,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"ureq",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1166,6 +1188,18 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
|
|
@ -1195,6 +1229,7 @@ checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
|||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ ureq = { version = "3.0.11", features = ["json"] }
|
|||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.20.0"
|
||||
uuid = { version = "1.17.0", features = ["rng", "std", "v1"] }
|
||||
|
||||
[profile.release]
|
||||
# Optimize for speed even more aggressively
|
||||
|
|
|
|||
10
src/error.rs
10
src/error.rs
|
|
@ -154,3 +154,13 @@ impl Error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error_response {
|
||||
// Match a format string plus optional arguments
|
||||
($fmt:expr $(, $arg:expr )* $(,)?) => {
|
||||
// Expand to constructing the Error::Response variant,
|
||||
// wrapping a `format!(...)` call
|
||||
Error::Response(format!($fmt $(, $arg )*))
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,12 +115,10 @@ impl FileTrait for Checked {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// tests for `Checked::start_upload` omitted, as they require a `sharry::Client`
|
||||
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::test_util::{
|
||||
create_file,
|
||||
MockClient, create_file,
|
||||
data::{HASHES_STD_GOOD, cases, data},
|
||||
};
|
||||
|
||||
|
|
@ -204,4 +202,25 @@ mod tests {
|
|||
assert!(err.is_mismatch("unhashed file", chk.path.display().to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_upload_works() {
|
||||
let client = MockClient::default();
|
||||
let share_id = client.add_share();
|
||||
|
||||
for content in data() {
|
||||
let file = create_file(content);
|
||||
let chk = Checked::new(file.path()).expect("creating `Checked` should succeed");
|
||||
|
||||
assert!(
|
||||
chk.start_upload(
|
||||
&client,
|
||||
&sharry::Uri::from(true),
|
||||
&sharry::AliasID::from(true),
|
||||
&share_id
|
||||
)
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,10 @@ mod tests {
|
|||
);
|
||||
check_trait(format!("{chunk:?}"), repr_expect, "Debug", "Chunk");
|
||||
|
||||
assert_eq!(chunk.get_file_id().to_string(), "");
|
||||
assert_eq!(
|
||||
chunk.get_file_id().to_string(),
|
||||
sharry::FileID::default().to_string()
|
||||
);
|
||||
assert_eq!(chunk.get_offset(), mock_offset);
|
||||
assert_eq!(chunk.get_data(), data);
|
||||
assert_eq!(chunk.get_length(), len);
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use std::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
Error, Result,
|
||||
Error, Result, error_response,
|
||||
file::{self, FileTrait},
|
||||
sharry::{AliasID, Client, FileID, ShareID, Uri, json},
|
||||
};
|
||||
|
||||
use super::mock_ids::CheckID;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct MockClient {
|
||||
shares: RefCell<HashMap<String, MockShare>>,
|
||||
}
|
||||
|
|
@ -23,82 +23,84 @@ struct MockShare {
|
|||
|
||||
#[derive(Debug)]
|
||||
struct MockFile {
|
||||
name: String,
|
||||
size: u64,
|
||||
offset: u64,
|
||||
}
|
||||
|
||||
impl From<&file::Checked> for MockFile {
|
||||
fn from(value: &file::Checked) -> Self {
|
||||
Self {
|
||||
size: value.get_size(),
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockClient {
|
||||
fn insert_share(&self, share_id: &ShareID) -> Result<()> {
|
||||
fn insert_share(&self, share_id: &ShareID, share: MockShare) -> Result<()> {
|
||||
let mut shares = self.shares.borrow_mut();
|
||||
|
||||
let Entry::Vacant(entry) = shares.entry(share_id.to_string()) else {
|
||||
return Err(Error::response(format_args!(
|
||||
"Can't create share {share_id:?}!"
|
||||
)));
|
||||
return Err(error_response!("can't insert share {share_id:?}!"));
|
||||
};
|
||||
|
||||
entry.insert(MockShare::default());
|
||||
entry.insert(share);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_file(
|
||||
&self,
|
||||
share_id: &ShareID,
|
||||
file_id: &FileID,
|
||||
name: String,
|
||||
size: u64,
|
||||
) -> Result<()> {
|
||||
let mut share = self.get_share_mut(share_id)?;
|
||||
fn insert_file(&self, share_id: &ShareID, file_id: &FileID, file: MockFile) -> Result<()> {
|
||||
let mut share = self.get_share(share_id)?;
|
||||
|
||||
let Entry::Vacant(entry) = share.files.entry(file_id.to_string()) else {
|
||||
return Err(Error::response(format_args!(
|
||||
"Can't create file {file_id:?}!"
|
||||
)));
|
||||
return Err(error_response!("can't insert file {file_id:?}!"));
|
||||
};
|
||||
|
||||
entry.insert(MockFile {
|
||||
name,
|
||||
size,
|
||||
offset: 0,
|
||||
});
|
||||
entry.insert(file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_share_mut<'t>(&'t self, share_id: &ShareID) -> Result<RefMut<'t, MockShare>> {
|
||||
fn get_share<'t>(&'t self, share_id: &ShareID) -> Result<RefMut<'t, MockShare>> {
|
||||
let share_id = &share_id.to_string();
|
||||
let shares = self.shares.borrow_mut();
|
||||
|
||||
// check share exists
|
||||
shares
|
||||
.get(share_id)
|
||||
.ok_or_else(|| Error::response(format_args!("Can't find share {share_id:?}!")))?;
|
||||
.ok_or_else(|| error_response!("can't find share {share_id:?}!"))?;
|
||||
|
||||
// share exists
|
||||
Ok(RefMut::map(shares, |shares| {
|
||||
shares.get_mut(share_id).expect("checked but None!")
|
||||
}))
|
||||
}
|
||||
|
||||
fn get_file_mut<'t>(
|
||||
fn get_file<'t>(
|
||||
&'t self,
|
||||
share_id: &ShareID,
|
||||
file_id: &FileID,
|
||||
) -> Result<RefMut<'t, MockFile>> {
|
||||
let file_id = &file_id.to_string();
|
||||
let share = self.get_share_mut(share_id)?;
|
||||
let share = self.get_share(share_id)?;
|
||||
|
||||
// check file exists
|
||||
share
|
||||
.files
|
||||
.get(file_id)
|
||||
.ok_or_else(|| Error::response(format_args!("Can't find file {file_id:?}!")))?;
|
||||
.ok_or_else(|| error_response!("can't find file {file_id:?}!"))?;
|
||||
|
||||
// file exists
|
||||
Ok(RefMut::map(share, move |share| {
|
||||
share.files.get_mut(file_id).expect("checked but None!")
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn add_share(&self) -> ShareID {
|
||||
let share_id = ShareID::from(true);
|
||||
self.insert_share(&share_id, MockShare::default())
|
||||
.expect("should never fail");
|
||||
|
||||
share_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Client for MockClient {
|
||||
|
|
@ -111,7 +113,7 @@ impl Client for MockClient {
|
|||
(uri, alias_id).check()?;
|
||||
|
||||
let share_id = true.into();
|
||||
self.insert_share(&share_id)?;
|
||||
self.insert_share(&share_id, MockShare::default())?;
|
||||
|
||||
Ok(share_id)
|
||||
}
|
||||
|
|
@ -120,6 +122,8 @@ impl Client for MockClient {
|
|||
(uri, alias_id).check()?;
|
||||
share_id.check()?;
|
||||
|
||||
let _share = self.get_share(share_id)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -134,12 +138,7 @@ impl Client for MockClient {
|
|||
share_id.check()?;
|
||||
|
||||
let file_id = true.into();
|
||||
self.insert_file(
|
||||
share_id,
|
||||
&file_id,
|
||||
file.get_name().to_string(),
|
||||
file.get_size(),
|
||||
)?;
|
||||
self.insert_file(share_id, &file_id, file.into())?;
|
||||
|
||||
Ok(file_id)
|
||||
}
|
||||
|
|
@ -154,11 +153,21 @@ impl Client for MockClient {
|
|||
(uri, alias_id).check()?;
|
||||
(share_id, chunk.get_file_id()).check()?;
|
||||
|
||||
// TODO: `chunk` must align to a full MiB
|
||||
let file = self.get_file_mut(share_id, chunk.get_file_id())?;
|
||||
let file = self.get_file(share_id, chunk.get_file_id())?;
|
||||
|
||||
todo!()
|
||||
if chunk.get_length() == 0 {
|
||||
return Err(error_response!("chunk {chunk:?} empty!"));
|
||||
} else if chunk.get_offset() % (1024 * 1024) != 0 {
|
||||
return Err(error_response!("chunk {chunk:?} not aligned to a MiB!"));
|
||||
} else if chunk.get_offset() != file.offset {
|
||||
return Error::mismatch(file.offset, chunk.get_offset());
|
||||
} else if file.offset + chunk.get_length() > file.size {
|
||||
return Err(error_response!("chunk {chunk:?} too long!"));
|
||||
}
|
||||
|
||||
// Ok(())
|
||||
let mut file = file;
|
||||
file.offset += chunk.get_length();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
Result,
|
||||
sharry::{AliasID, FileID, ShareID, Uri},
|
||||
|
|
@ -10,11 +12,25 @@ const VALID_FILE: &str = "valid-file";
|
|||
|
||||
fn make_invalid(valid: &str) -> String {
|
||||
let invalid = valid.replace("valid", "invalid");
|
||||
assert_ne!(valid, invalid);
|
||||
|
||||
assert_ne!(invalid, valid);
|
||||
|
||||
invalid
|
||||
}
|
||||
|
||||
fn make_valid(valid: &str) -> String {
|
||||
let invalid = make_invalid(valid);
|
||||
|
||||
let valid = {
|
||||
let id = Uuid::now_v1(&[4, 8, 15, 16, 23, 42]);
|
||||
valid.replace("valid", &id.to_string())
|
||||
};
|
||||
|
||||
assert_ne!(valid, invalid);
|
||||
|
||||
valid
|
||||
}
|
||||
|
||||
pub trait CheckID {
|
||||
fn check(self) -> Result<()>;
|
||||
}
|
||||
|
|
@ -56,7 +72,7 @@ impl CheckID for (&ShareID, &FileID) {
|
|||
impl From<bool> for Uri {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::from(VALID_URI.to_string())
|
||||
Self::from(make_valid(VALID_URI))
|
||||
} else {
|
||||
Self::from(make_invalid(VALID_URI))
|
||||
}
|
||||
|
|
@ -66,7 +82,7 @@ impl From<bool> for Uri {
|
|||
impl From<bool> for AliasID {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::from(VALID_ALIAS.to_string())
|
||||
Self::from(make_valid(VALID_ALIAS))
|
||||
} else {
|
||||
Self::from(make_invalid(VALID_ALIAS))
|
||||
}
|
||||
|
|
@ -76,7 +92,7 @@ impl From<bool> for AliasID {
|
|||
impl From<bool> for ShareID {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::from(VALID_SHARE.to_string())
|
||||
Self::from(make_valid(VALID_SHARE))
|
||||
} else {
|
||||
Self::from(make_invalid(VALID_SHARE))
|
||||
}
|
||||
|
|
@ -86,7 +102,7 @@ impl From<bool> for ShareID {
|
|||
impl From<bool> for FileID {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Self::new_test(VALID_FILE)
|
||||
Self::new_test(make_valid(VALID_FILE))
|
||||
} else {
|
||||
Self::new_test(make_invalid(VALID_FILE))
|
||||
}
|
||||
|
|
@ -106,61 +122,60 @@ mod tests {
|
|||
let share_id = ShareID::from(true);
|
||||
let file_id = FileID::from(true);
|
||||
|
||||
assert!(matches!((&uri, &alias_id).check(), Ok(())));
|
||||
assert!(matches!(share_id.check(), Ok(())));
|
||||
assert!(matches!((&share_id, &file_id).check(), Ok(())));
|
||||
assert!((&uri, &alias_id).check().is_ok());
|
||||
assert!(share_id.check().is_ok());
|
||||
assert!((&share_id, &file_id).check().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_valid() {
|
||||
let uri = Uri::default();
|
||||
let alias_id = AliasID::from(true); // no `impl Default`
|
||||
let share_id = ShareID::default();
|
||||
let file_id = FileID::default();
|
||||
|
||||
assert!(matches!((&uri, &AliasID::from(true)).check(), Ok(())));
|
||||
assert!(matches!(share_id.check(), Ok(())));
|
||||
assert!(matches!((&share_id, &file_id).check(), Ok(())));
|
||||
assert!((&uri, &alias_id).check().is_ok());
|
||||
assert!(share_id.check().is_ok());
|
||||
assert!((&share_id, &file_id).check().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn false_makes_invalids() {
|
||||
fn test_check(value: impl CheckID, callback: impl FnOnce(&Parameter) -> bool) {
|
||||
let check = value.check().expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(callback(p));
|
||||
}
|
||||
|
||||
// valid ids
|
||||
let uri = Uri::from(true);
|
||||
let alias_id = AliasID::from(true);
|
||||
let share_id = ShareID::from(true);
|
||||
let file_id = FileID::from(true);
|
||||
|
||||
// invalid ids
|
||||
let uri_i = Uri::from(false);
|
||||
let alias_id_i = AliasID::from(false);
|
||||
let share_id_i = ShareID::from(false);
|
||||
let file_id_i = FileID::from(false);
|
||||
|
||||
// invalid Uri, valid alias
|
||||
let check = (&uri_i, &alias_id).check().expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(matches!(p, Parameter::Uri(_)));
|
||||
// param checks
|
||||
let is_uri_i = |p: &Parameter| matches!(p, Parameter::Uri(_));
|
||||
let is_alias_id_i = |p: &Parameter| matches!(p, Parameter::AliasID(_));
|
||||
let is_share_id_i = |p: &Parameter| matches!(p, Parameter::ShareID(_));
|
||||
let is_file_id_i = |p: &Parameter| matches!(p, Parameter::FileID(_));
|
||||
|
||||
// valid Uri, invalid alias
|
||||
let check = (&uri, &alias_id_i).check().expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(matches!(p, Parameter::AliasID(_)));
|
||||
// uri + alias
|
||||
test_check((&uri_i, &alias_id_i), is_uri_i);
|
||||
test_check((&uri_i, &alias_id), is_uri_i);
|
||||
test_check((&uri, &alias_id_i), is_alias_id_i);
|
||||
|
||||
// invalid share
|
||||
let check = share_id_i.check().expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(matches!(p, Parameter::ShareID(_)));
|
||||
// share
|
||||
test_check(&share_id_i, is_share_id_i);
|
||||
|
||||
// invalid share, valid file
|
||||
let check = (&share_id_i, &file_id)
|
||||
.check()
|
||||
.expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(matches!(p, Parameter::ShareID(_)));
|
||||
|
||||
// valid share, invalid file
|
||||
let check = (&share_id, &file_id_i)
|
||||
.check()
|
||||
.expect_err("should be invalid");
|
||||
let p = check.get_invalid_param().expect("should be InvalidParam");
|
||||
assert!(matches!(p, Parameter::FileID(_)));
|
||||
// share + file
|
||||
test_check((&share_id_i, &file_id_i), is_share_id_i);
|
||||
test_check((&share_id_i, &file_id), is_share_id_i);
|
||||
test_check((&share_id, &file_id_i), is_file_id_i);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ pub mod data;
|
|||
mod mock_client;
|
||||
mod mock_ids;
|
||||
|
||||
pub use mock_client::MockClient;
|
||||
|
||||
use std::{fmt, io::Write};
|
||||
|
||||
use tempfile::NamedTempFile;
|
||||
|
|
|
|||
Loading…
Reference in a new issue