Browse Source

Mostly improved tree walking I guess.

master
Draklaw 2 years ago
parent
commit
25c7d0d470
  1. 73
      Cargo.lock
  2. 9
      cas-core/src/cas.rs
  3. 12
      cas-core/src/error.rs
  4. 6
      cas-core/src/lib.rs
  5. 2
      cas-core/src/object_metadata.rs
  6. 4
      cas-simple/Cargo.toml
  7. 182
      cas-simple/src/cas.rs
  8. 2
      cas-simple/src/lib.rs
  9. 102
      cas-simple/src/utils.rs
  10. 4
      libbsv/Cargo.toml
  11. 31
      libbsv/src/config.rs
  12. 2
      libbsv/src/ignore.rs
  13. 2
      libbsv/src/lib.rs
  14. 66
      libbsv/src/path_map.rs
  15. 206
      libbsv/src/repository.rs
  16. 121
      libbsv/src/tree_item.rs
  17. 72
      libbsv/src/tree_walker.rs

73
Cargo.lock

@ -93,6 +93,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.2" version = "0.3.2"
@ -130,6 +136,22 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]] [[package]]
name = "libbsv" name = "libbsv"
version = "0.1.0" version = "0.1.0"
@ -225,9 +247,18 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.127" version = "1.0.183"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c"
[[package]]
name = "serde_spanned"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
@ -288,11 +319,36 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
dependencies = [ dependencies = [
"serde", "serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
] ]
[[package]] [[package]]
@ -378,3 +434,12 @@ name = "windows_x86_64_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5504cc7644f4b593cbc05c4a55bf9bd4e94b867c3c0bd440934174d50482427d"
dependencies = [
"memchr",
]

9
cas-core/src/cas.rs

@ -63,7 +63,10 @@ pub trait Cas {
} }
pub trait RefStore { pub trait RefStore {
fn get_ref<P: AsRef<Utf8Path>>(&self, key: P) -> Result<ObjectId>; fn get_ref(&self, key: &Utf8Path) -> Result<ObjectId>;
fn set_ref<P: AsRef<Utf8Path>>(&mut self, key: P, value: &ObjectId) -> Result<()>; fn set_ref(&mut self, key: &Utf8Path, value: &ObjectId) -> Result<()>;
fn remove_ref<P: AsRef<Utf8Path>>(&mut self, key: P) -> Result<()>; fn remove_ref(&mut self, key: &Utf8Path) -> Result<()>;
}
pub trait CasWithRef: Cas + RefStore {
} }

12
cas-core/src/error.rs

@ -16,6 +16,10 @@
// use std::path::Utf8PathBuf; // use std::path::Utf8PathBuf;
use std::path::StripPrefixError;
use crate::ObjectId;
/// Result type used through cas-core. /// Result type used through cas-core.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -87,6 +91,9 @@ pub enum Error {
#[error("non-unicode file name: '{0}'")] #[error("non-unicode file name: '{0}'")]
NonUnicodeFileName(String), NonUnicodeFileName(String),
#[error("object {0} does not exists")]
ObjectDoesNotExists(ObjectId),
#[error("{0}")] #[error("{0}")]
UnknownError(String), UnknownError(String),
} }
@ -129,6 +136,11 @@ impl Error {
} }
} }
impl From<StripPrefixError> for Error {
fn from(value: StripPrefixError) -> Self {
Error::unknown(format!("Invalid operation: {}", value))
}
}
// fn format_optional_path(maybe_path: &Option<Utf8PathBuf>) -> String { // fn format_optional_path(maybe_path: &Option<Utf8PathBuf>) -> String {
// match maybe_path { // match maybe_path {

6
cas-core/src/lib.rs

@ -35,10 +35,10 @@ mod cas;
pub use crate::{ pub use crate::{
error::{Error, Result}, error::{Error, Result},
object_id::{ObjectId, hex, write_hex}, object_id::{ObjectId, hex, write_hex},
object_type::{ObjectType}, object_type::ObjectType,
object_metadata::{ObjectMetadata}, object_metadata::ObjectMetadata,
pipeline::{Pipeline, DefaultPipeline, Reader, Writer, ReadWrapper, WriteWrapper}, pipeline::{Pipeline, DefaultPipeline, Reader, Writer, ReadWrapper, WriteWrapper},
cas::{Cas, RefStore}, cas::{Cas, CasWithRef, RefStore},
}; };

2
cas-core/src/object_metadata.rs

@ -14,7 +14,7 @@
// along with bsv. If not, see <https://www.gnu.org/licenses/>. // along with bsv. If not, see <https://www.gnu.org/licenses/>.
use super::object_type::{ObjectType}; use super::object_type::ObjectType;
#[derive(Clone, Eq, PartialEq)] #[derive(Clone, Eq, PartialEq)]

4
cas-simple/Cargo.toml

@ -11,8 +11,6 @@ license = "AGPL-3.0-or-later"
digest = { version = "0.9.0", features = ["alloc"] } digest = { version = "0.9.0", features = ["alloc"] }
sha2 = "0.9.5" sha2 = "0.9.5"
camino = { version = "1.0.7" } camino = { version = "1.0.7" }
toml = "0.5.8" toml = "0.7.6"
cas-core = { path = "../cas-core" } cas-core = { path = "../cas-core" }
tempfile = "3.2.0" tempfile = "3.2.0"
[dev-dependencies]

182
cas-simple/src/cas.rs

@ -17,60 +17,75 @@
use std::str::FromStr; use std::str::FromStr;
use digest::DynDigest; use digest::DynDigest;
use toml::Value; use toml::{Value, Table};
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{ use cas_core::{
Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType, Cas, DefaultPipeline, err, Error, ObjectId, ObjectMetadata, ObjectType,
Pipeline, Reader, ReadWrapper, RefStore, Result, Writer, Pipeline, Reader, ReadWrapper, RefStore, Result, Writer, CasWithRef,
}; };
use crate::utils::{ use crate::utils::{
obj_dir, obj_path, ref_dir, tmp_dir, obj_dir, obj_path, ref_dir, tmp_dir,
read_config, write_config,
read_metadata, write_metadata, read_metadata, write_metadata,
new_digest, new_digest,
}; };
use crate::wfile::WFile; use crate::wfile::WFile;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SimpleCasConfig {
pub digest_id: String,
}
impl SimpleCasConfig {
pub fn new(digest_id: String) -> Self {
Self {
digest_id,
}
}
pub fn from_toml_value(value: &Value) -> Result<Self> {
if let Value::Table(ref table) = value {
let engine =
table.get("engine")
.ok_or_else(|| Error::unknown("missing engine field in cas config"))?
.as_str()
.ok_or_else(|| Error::unknown("cas engine must be a string"))?;
if engine != "simple" {
return err!("expected simple cas engine");
}
let digest_id =
table.get("digest")
.ok_or_else(|| Error::unknown("missing digest field in cas config"))?
.as_str()
.ok_or_else(|| Error::unknown("cas digest must be a string"))?;
Ok(Self {
digest_id: digest_id.to_string(),
})
}
else {
err!("cas config must be a table")
}
}
pub fn to_toml_value(&self) -> Result<Value> {
let mut table = Table::new();
table["engine"] = "simple".into();
table["digest"] = "sha256".into();
return Ok(Value::Table(table))
}
}
pub struct SimpleCas { pub struct SimpleCas {
config: SimpleCasConfig,
db_path: Utf8PathBuf, db_path: Utf8PathBuf,
digest: Box<dyn DynDigest>, digest: Box<dyn DynDigest>,
pipeline: DefaultPipeline, pipeline: DefaultPipeline,
// config: Value,
} }
impl SimpleCas { impl SimpleCas {
pub fn create(db_path: Utf8PathBuf, mut config: Value) -> Result<Self> { pub fn create(db_path: Utf8PathBuf, config: SimpleCasConfig) -> Result<Self> {
if !config.is_table() {
return Error::err("invalid config object: must be table");
}
let maybe_engine = config.as_table_mut().unwrap()
.entry("cas")
.or_insert_with(|| toml::value::Table::new().into())
.as_table_mut().unwrap()
.entry("engine")
.or_insert("simple".into())
.as_str();
match maybe_engine {
Some(engine) if engine != "simple" => {
return err!("invalid cas.engine in config: got {}, expected simple", engine);
},
None => {
return Error::err("invalid casengine in config: expected String");
},
_ => {}
}
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::unknown(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone());
if db_path.exists() { if db_path.exists() {
return err!( return err!(
"failed to create SimpleCas: target directory already exists ({})", "failed to create SimpleCas: target directory already exists ({})",
@ -83,8 +98,8 @@ impl SimpleCas {
&obj_dir(&db_path), &obj_dir(&db_path),
&ref_dir(&db_path), &ref_dir(&db_path),
&tmp_dir(&db_path), &tmp_dir(&db_path),
] { ] {
std::fs::create_dir(path).or_else(|e| std::fs::create_dir(path).or_else(|e|
err!( err!(
"failed to create directory ({}): {}", "failed to create directory ({}): {}",
path, e, path, e,
@ -92,37 +107,24 @@ impl SimpleCas {
)?; )?;
} }
write_config(&config, &db_path)?; Self::open(db_path, config)
Ok(SimpleCas {
db_path,
digest,
pipeline,
// config,
})
} }
pub fn open(db_path: Utf8PathBuf) -> Result<Self> { pub fn open(db_path: Utf8PathBuf, config: SimpleCasConfig) -> Result<Self> {
let config = read_config(&db_path)?; let digest = new_digest(&config.digest_id)?;
let digest_id = config["cas"]["digest"].as_str()
.ok_or_else(|| Error::unknown(
"mandatory cas.digest value is invalid or missing from config"
))?;
let digest = new_digest(digest_id)?;
let pipeline = DefaultPipeline::new(digest.box_clone()); let pipeline = DefaultPipeline::new(digest.box_clone());
Ok(SimpleCas { Ok(SimpleCas {
config,
db_path, db_path,
digest, digest,
pipeline, pipeline,
// config,
}) })
} }
// pub fn save_config(&self) -> Result<()> { pub fn get_config(&self) -> &SimpleCasConfig {
// write_config(&self.config, &self.db_path) &self.config
// } }
} }
@ -152,7 +154,7 @@ impl Cas for SimpleCas {
fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box<dyn Reader>)> { fn open_object(&self, oid: &ObjectId) -> Result<(ObjectMetadata, Box<dyn Reader>)> {
let opath = obj_path(&self.db_path, oid); let opath = obj_path(&self.db_path, oid);
if !opath.is_file() { if !opath.is_file() {
return err!("object not found: {}", oid); return Err(Error::ObjectDoesNotExists(oid.clone()));
} }
let file = std::fs::File::open(opath).or_else(|err| let file = std::fs::File::open(opath).or_else(|err|
@ -184,56 +186,59 @@ impl Cas for SimpleCas {
} }
impl RefStore for SimpleCas { impl RefStore for SimpleCas {
fn get_ref<P: AsRef<Utf8Path>>(&self, key: P) -> Result<ObjectId> { fn get_ref(&self, key: &Utf8Path) -> Result<ObjectId> {
let path = ref_dir(&self.db_path).join(key.as_ref()); let path = ref_dir(&self.db_path).join(key);
if !path.exists() { if !path.exists() {
err!("reference {} does not exists", key.as_ref()) err!("reference {} does not exists", key)
} }
else if !path.is_file() { else if !path.is_file() {
err!("reference {} is not a file", key.as_ref()) err!("reference {} is not a file", key)
} }
else { else {
let file = std::fs::read(path).or_else(|err| let file = std::fs::read(path).or_else(|err|
err!("failed to read reference file for {}: {}", key.as_ref(), err) err!("failed to read reference file for {}: {}", key, err)
)?; )?;
Ok( Ok(
ObjectId::from_str( ObjectId::from_str(
std::str::from_utf8(&file).or_else(|err| std::str::from_utf8(&file).or_else(|err|
err!("invalid reference file at {}: {}", key.as_ref(), err) err!("invalid reference file at {}: {}", key, err)
)? )?
)? )?
) )
} }
} }
fn set_ref<P: AsRef<Utf8Path>>(&mut self, key: P, value: &ObjectId) -> Result<()> { fn set_ref(&mut self, key: &Utf8Path, value: &ObjectId) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref()); let path = ref_dir(&self.db_path).join(key);
std::fs::create_dir_all(path.parent().ok_or_else(|| std::fs::create_dir_all(path.parent().ok_or_else(||
Error::unknown(format!("reference file {} has no parent dir?", key.as_ref())) Error::unknown(format!("reference file {} has no parent dir?", key))
)?).or_else(|err| )?).or_else(|err|
err!("failed to create reference dir for {}: {}", key.as_ref(), err) err!("failed to create reference dir for {}: {}", key, err)
)?; )?;
std::fs::write(path, value.to_string()).or_else(|err| std::fs::write(path, value.to_string()).or_else(|err|
err!("failed to write reference {}: {}", key.as_ref(), err) err!("failed to write reference {}: {}", key, err)
) )
} }
fn remove_ref<P: AsRef<Utf8Path>>(&mut self, key: P) -> Result<()> { fn remove_ref(&mut self, key: &Utf8Path) -> Result<()> {
let path = ref_dir(&self.db_path).join(key.as_ref()); let path = ref_dir(&self.db_path).join(key);
if !path.exists() { if !path.exists() {
err!("reference {} does not exists", key.as_ref()) err!("reference {} does not exists", key)
} }
else if !path.is_file() { else if !path.is_file() {
err!("reference {} is not a file", key.as_ref()) err!("reference {} is not a file", key)
} }
else { else {
std::fs::remove_file(path).or_else(|err| std::fs::remove_file(path).or_else(|err|
err!("failed to remove reference file {}: {}", key.as_ref(), err) err!("failed to remove reference file {}: {}", key, err)
) )
} }
} }
} }
impl CasWithRef for SimpleCas {
}
pub struct ObjectIdIterator { pub struct ObjectIdIterator {
root_dirs: Vec<Utf8PathBuf>, root_dirs: Vec<Utf8PathBuf>,
@ -387,13 +392,6 @@ mod tests {
use super::*; use super::*;
fn get_config() -> Value {
toml::toml!(
[cas]
digest = "sha256"
)
}
fn get_cas_path(dir: &Utf8Path) -> Utf8PathBuf { fn get_cas_path(dir: &Utf8Path) -> Utf8PathBuf {
let mut cas_path = dir.to_path_buf(); let mut cas_path = dir.to_path_buf();
cas_path.push(".bsv"); cas_path.push(".bsv");
@ -404,7 +402,7 @@ mod tests {
fn test_create_simple_cas() { fn test_create_simple_cas() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(Utf8Path::from_path(dir.path()).unwrap()); let cas_path = get_cas_path(Utf8Path::from_path(dir.path()).unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let cas = SimpleCas::create(cas_path, config) let cas = SimpleCas::create(cas_path, config)
.expect("failed to create SimpleCas object"); .expect("failed to create SimpleCas object");
@ -418,7 +416,7 @@ mod tests {
fn test_write_object() { fn test_write_object() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type"); let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"Hello World!"; let payload = b"Hello World!";
@ -448,7 +446,7 @@ mod tests {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type"); let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"Hello World!"; let payload = b"Hello World!";
@ -487,7 +485,7 @@ mod tests {
fn test_read_write_object() { fn test_read_write_object() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type"); let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"This is a test."; let payload = b"This is a test.";
@ -507,7 +505,7 @@ mod tests {
fn test_remove_object() { fn test_remove_object() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let otype = ObjectType::new(b"blob").expect("failed to create object type"); let otype = ObjectType::new(b"blob").expect("failed to create object type");
let payload = b"This is a test."; let payload = b"This is a test.";
@ -528,7 +526,7 @@ mod tests {
fn test_object_id_iterator() { fn test_object_id_iterator() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let mut cas = SimpleCas::create(cas_path.clone(), config) let mut cas = SimpleCas::create(cas_path.clone(), config)
.expect("failed to create SimpleCas object"); .expect("failed to create SimpleCas object");
@ -642,21 +640,21 @@ mod tests {
fn test_reference() { fn test_reference() {
let dir = tempfile::tempdir().expect("failed to create temp test dir"); let dir = tempfile::tempdir().expect("failed to create temp test dir");
let cas_path = get_cas_path(dir.path().try_into().unwrap()); let cas_path = get_cas_path(dir.path().try_into().unwrap());
let config = get_config(); let config = SimpleCasConfig::new("sha256".into());
let mut cas = SimpleCas::create(cas_path.clone(), config) let mut cas = SimpleCas::create(cas_path.clone(), config)
.expect("failed to create SimpleCas object"); .expect("failed to create SimpleCas object");
let oid_0 = ObjectId::from_str("f731f6bc6a6a73bad170e56452473ef6930b7a0ab33cc54be44221a89b49d786").unwrap(); let oid_0 = ObjectId::from_str("f731f6bc6a6a73bad170e56452473ef6930b7a0ab33cc54be44221a89b49d786").unwrap();
assert!(cas.get_ref("foo/bar").is_err()); assert!(cas.get_ref("foo/bar".into()).is_err());
assert!(cas.remove_ref("foo/bar").is_err()); assert!(cas.remove_ref("foo/bar".into()).is_err());
cas.set_ref("foo/bar", &oid_0).unwrap(); cas.set_ref("foo/bar".into(), &oid_0).unwrap();
assert_eq!(cas.get_ref("foo/bar").unwrap(), oid_0); assert_eq!(cas.get_ref("foo/bar".into()).unwrap(), oid_0);
cas.remove_ref("foo/bar").unwrap(); cas.remove_ref("foo/bar".into()).unwrap();
assert!(cas.get_ref("foo/bar").is_err()); assert!(cas.get_ref("foo/bar".into()).is_err());
} }
} }

2
cas-simple/src/lib.rs

@ -32,4 +32,4 @@ mod wfile;
mod cas; mod cas;
pub use cas::SimpleCas; pub use cas::{SimpleCas, SimpleCasConfig};

102
cas-simple/src/utils.rs

@ -16,7 +16,7 @@
use digest::DynDigest; use digest::DynDigest;
use toml::Value; // use toml::Value;
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{ use cas_core::{
@ -49,39 +49,39 @@ pub fn config_path(cas_path: &Utf8Path) -> Utf8PathBuf {
} }
pub fn read_config(db_path: &Utf8Path) -> Result<Value> { // pub fn read_config(db_path: &Utf8Path) -> Result<Value> {
use std::io::Read; // use std::io::Read;
let mut file = std::fs::File::open(config_path(db_path)).or_else(|err| // let mut file = std::fs::File::open(config_path(db_path)).or_else(|err|
err!("invalid repository: no config file: {}", err) // err!("invalid repository: no config file: {}", err)
)?; // )?;
let mut config_str = String::new(); // let mut config_str = String::new();
file.read_to_string(&mut config_str).or_else(|err| // file.read_to_string(&mut config_str).or_else(|err|
err!("cannot read config file: {}", err) // err!("cannot read config file: {}", err)
)?; // )?;
let config = toml::from_str(&config_str).or_else(|err| // let config = toml::from_str(&config_str).or_else(|err|
err!("error while reading config file: {}", err) // err!("error while reading config file: {}", err)
)?; // )?;
Ok(config) // Ok(config)
} // }
pub fn write_config(config: &Value, db_path: &Utf8Path) -> Result<()> { // pub fn write_config(config: &Value, db_path: &Utf8Path) -> Result<()> {
use std::io::Write; // use std::io::Write;
let config_str = toml::to_string_pretty(config).or_else(|err| // let config_str = toml::to_string_pretty(config).or_else(|err|
err!("failed to serialize config: {}", err) // err!("failed to serialize config: {}", err)
)?; // )?;
let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err| // let mut file = tempfile::NamedTempFile::new_in(tmp_dir(db_path)).or_else(|err|
err!("cannot create temp config file: {}", err) // err!("cannot create temp config file: {}", err)
)?; // )?;
file.write_all(config_str.as_bytes()).or_else(|err| // file.write_all(config_str.as_bytes()).or_else(|err|
err!("failed to write to temp config: {}", err) // err!("failed to write to temp config: {}", err)
)?; // )?;
file.persist(config_path(db_path)).or_else(|err| // file.persist(config_path(db_path)).or_else(|err|
err!("failed to (over)write config: {}", err) // err!("failed to (over)write config: {}", err)
)?; // )?;
Ok(()) // Ok(())
} // }
pub fn read_metadata(read: &mut dyn std::io::Read) -> Result<ObjectMetadata> { pub fn read_metadata(read: &mut dyn std::io::Read) -> Result<ObjectMetadata> {
@ -151,28 +151,28 @@ mod tests {
); );
} }
#[test] // #[test]
fn test_read_write_config() { // fn test_read_write_config() {
let config = toml::toml!{ // let config = toml::toml!{
[cas] // [cas]
path = "/foo/bar" // path = "/foo/bar"
digest = "sha1" // digest = "sha1"
[extra] // [extra]
test = 42 // test = 42
}; // };
let dir = tempfile::TempDir::new() // let dir = tempfile::TempDir::new()
.expect("failed to create tmp dir"); // .expect("failed to create tmp dir");
std::fs::create_dir(tmp_dir(Utf8Path::from_path(dir.path()).unwrap())).expect("failed to create db/tmp dir"); // std::fs::create_dir(tmp_dir(Utf8Path::from_path(dir.path()).unwrap())).expect("failed to create db/tmp dir");
write_config(&config, Utf8Path::from_path(dir.path()).unwrap()).expect("failed to write config"); // write_config(&config, Utf8Path::from_path(dir.path()).unwrap()).expect("failed to write config");
let config2 = read_config(Utf8Path::from_path(dir.path()).unwrap()).expect("failed to read config"); // let config2 = read_config(Utf8Path::from_path(dir.path()).unwrap()).expect("failed to read config");
assert_eq!(config2, config); // assert_eq!(config2, config);
dir.close().expect("failed to close tmp dir") // dir.close().expect("failed to close tmp dir")
} // }
#[test] #[test]
fn test_read_write_metadata() { fn test_read_write_metadata() {

4
libbsv/Cargo.toml

@ -6,11 +6,9 @@ edition = "2021"
license = "AGPL-3.0-or-later" license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
toml = "0.5.8" toml = { version = "0.7.6", features = ["parse"] }
camino = "1.0.7" camino = "1.0.7"
regex = "1.6.0" regex = "1.6.0"
cas-core = { path = "../cas-core" } cas-core = { path = "../cas-core" }
cas-simple = { path = "../cas-simple" } cas-simple = { path = "../cas-simple" }
[dev-dependencies]
tempfile = "3.7.1" tempfile = "3.7.1"

31
libbsv/src/config.rs

@ -1,31 +0,0 @@
// This file is part of bsv.
//
// bsv is free software: you can redistribute it and/or modify it under the
// terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// bsv is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
// more details.
//
// You should have received a copy of the Affero GNU General Public License
// along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::collections::{HashMap};
use camino::Utf8PathBuf;
use cas_core::{Cas, Result};
pub trait CasConfig {
fn build_cas(&self) -> Result<Box<dyn Cas>>;
}
pub struct BsvConfig {
cas: Box<dyn CasConfig>,
dir_map: HashMap<Utf8PathBuf, Utf8PathBuf>,
}

2
libbsv/src/ignore.rs

@ -27,7 +27,7 @@ pub enum IgnoreAction {
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct IgnoreRules { pub struct IgnoreRules {
patterns: RegexSet, patterns: RegexSet,
actions: Vec<IgnoreAction>, actions: Vec<IgnoreAction>,

2
libbsv/src/lib.rs

@ -17,8 +17,6 @@
extern crate toml; extern crate toml;
extern crate camino; extern crate camino;
extern crate regex; extern crate regex;
#[cfg(test)]
extern crate tempfile; extern crate tempfile;
extern crate cas_core; extern crate cas_core;

66
libbsv/src/path_map.rs

@ -15,7 +15,7 @@
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use toml::Value; use toml::{Value, Table};
use cas_core::{err, Error, Result}; use cas_core::{err, Error, Result};
@ -30,10 +30,28 @@ impl PathPair {
pub fn new(logic: Utf8PathBuf, physic: Utf8PathBuf) -> Self { pub fn new(logic: Utf8PathBuf, physic: Utf8PathBuf) -> Self {
Self { logic, physic } Self { logic, physic }
} }
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Result<Utf8PathBuf> {
let mut physic = self.physic.clone();
physic.push(
logic_path.as_ref().strip_prefix(&self.logic)?
);
Ok(physic)
}
pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<Utf8PathBuf> {
let mut logic = self.logic.clone();
logic.push(
physic_path.as_ref().strip_prefix(&self.physic)?
);
Ok(logic)
}
} }
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct PathMap { pub struct PathMap {
pairs: Vec<PathPair>, pairs: Vec<PathPair>,
logic_order: Vec<usize>, logic_order: Vec<usize>,
@ -113,38 +131,42 @@ impl PathMap {
Self::from_vec(map) Self::from_vec(map)
} }
pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> { pub fn to_toml_value(&self) -> Result<Value> {
let mut table = Table::new();
for pair in self.pairs.iter() {
table[pair.logic.as_str()] = pair.physic.as_str().into();
}
Ok(Value::Table(table))
}
pub fn path_pair_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<&PathPair> {
let path = logic_path.as_ref(); let path = logic_path.as_ref();
let pair = self.logic_order.iter() Some(self.logic_order.iter()
.map(|&i| &self.pairs[i]) .map(|&i| &self.pairs[i])
.filter(|p| path.starts_with(&p.logic)) .filter(|p| path.starts_with(&p.logic))
.next()?; .next()?
)
let mut physic = pair.physic.clone();
physic.push(
path.strip_prefix(&pair.logic)
.ok()?
);
return Some(physic)
} }
pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> { pub fn path_pair_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<&PathPair> {
let path = physic_path.as_ref(); let path = physic_path.as_ref();
let pair = self.physic_order.iter() Some(self.physic_order.iter()
.map(|&i| &self.pairs[i]) .map(|&i| &self.pairs[i])
.filter(|p| path.starts_with(&p.physic)) .filter(|p| path.starts_with(&p.physic))
.next()?; .next()?
)
}
let mut logic = pair.logic.clone(); pub fn physic_from_logic<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
logic.push( self.path_pair_from_logic(&logic_path)?
path.strip_prefix(&pair.physic) .physic_from_logic(&logic_path).ok()
.ok()? }
);
return Some(logic) pub fn logic_from_physic<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
self.path_pair_from_physic(&physic_path)?
.logic_from_physic(&physic_path).ok()
} }
} }

206
libbsv/src/repository.rs

@ -14,100 +14,118 @@
// along with bsv. If not, see <https://www.gnu.org/licenses/>. // along with bsv. If not, see <https://www.gnu.org/licenses/>.
use std::{fs::create_dir_all, io::BufReader};
use camino::{Utf8Path, Utf8PathBuf}; use camino::{Utf8Path, Utf8PathBuf};
use toml::Value; use cas_simple::utils::config_path;
use toml::{Value, Table};
use cas_core::{Cas, err, Error, ObjectId, Result}; use cas_core::{err, Error, ObjectId, Result, ObjectType, CasWithRef};
use cas_simple::SimpleCas; use cas_simple::{SimpleCas, SimpleCasConfig};
use crate::PathMap;
pub use crate::permissions::Permissions; pub use crate::permissions::Permissions;
use crate::tree_item::Deserialize;
pub use crate::tree_item::{Serialize, TreeItem}; pub use crate::tree_item::{Serialize, TreeItem};
pub fn create_cas(path: Utf8PathBuf, config: Value) -> Result<Box<dyn Cas>> { #[derive(Clone, Debug)]
let engine = config pub struct Config {
.get("cas") pub device_name: String,
.ok_or_else(|| Error::unknown("config must have a cas item"))? pub cas: SimpleCasConfig,
.get("engine") pub path_map: PathMap,
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))?
.as_str()
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?;
match engine {
"simple" => { SimpleCas::create(path, config).map(|cas| Box::new(cas) as Box<dyn Cas>) }
_ => { err!("unknown cas engine {}", engine) }
}
} }
impl Config {
pub fn from_toml_value(value: &Value) -> Result<Self> {
let bsv_value = value.get("bsv")
.ok_or_else(|| Error::unknown("bsv section missing from config"))?;
if !bsv_value.is_table() {
return err!("bsv section must be a table");
}
pub fn open_cas(path: Utf8PathBuf, config: &Value) -> Result<Box<dyn Cas>> { Ok(Self {
let engine = config device_name: bsv_value.get("device_name")
.get("cas") .ok_or_else(|| Error::unknown("bsv.device_name missing from config"))?
.ok_or_else(|| Error::unknown("config must have a cas item"))? .as_str()
.get("engine") .ok_or_else(|| Error::unknown("bsv.device_name must be a string"))?
.ok_or_else(|| Error::unknown("config must have a cas.engine item"))? .to_string(),
.as_str() cas: SimpleCasConfig::from_toml_value(
.ok_or_else(|| Error::unknown("cas.engine must be a string"))?; value.get("cas").ok_or_else(|| Error::unknown("config must have a cas section"))?
)?,
path_map: PathMap::from_toml_value(
value.get("mapping").ok_or_else(|| Error::unknown("config must have a mapping section"))?
)?,
})
}
match engine { pub fn from_toml_file(config_path: &Utf8Path) -> Result<Self> {
"simple" => { SimpleCas::open(path).map(|cas| Box::new(cas) as Box<dyn Cas>) } use std::io::Read;
_ => { err!("unknown cas engine {}", engine) }
let mut file = std::fs::File::open(config_path).or_else(|err|
err!("invalid repository: failed to read config file: {}", err)
)?;
let mut config_str = String::new();
file.read_to_string(&mut config_str).or_else(|err|
err!("failed to read config file: {}", err)
)?;
let value = config_str.parse::<Value>().or_else(|err|
err!("parse error while reading config file: {}", err)
)?;
Self::from_toml_value(&value)
} }
}
pub fn to_toml_value(&self) -> Result<Value> {
let mut bsv = Table::new();
bsv["device_name"] = Value::String(self.device_name.clone());
let mut table = Table::new();
table["bsv"] = Value::Table(bsv);
table["cas"] = self.cas.to_toml_value()?;
table["mapping"] = self.path_map.to_toml_value()?;
Ok(Value::Table(table))
}
// pub trait FsVisitor { pub fn write_toml_file(&self, config_path: &Utf8Path) -> Result<()> {
// fn accept(&self, path: &Utf8Path, metadata: &Metadata) -> bool; use std::io::Write;
// fn handle_error(&mut self, error: Error) -> Option<Error>;
let value = self.to_toml_value()?;
// fn handle_result<T>(&mut self, result: Result<T>) -> Result<T> let config_str = toml::to_string_pretty(&value).or_else(|err|
// { err!("failed to serialize config: {}", err)
// result.map_err(|error| )?;
// self.handle_error(error) let mut file = tempfile::NamedTempFile::new_in(config_path.parent().unwrap()).or_else(|err|
// .unwrap_or(Error::Skipped) err!("cannot create temp config file: {}", err)
// ) )?;
// } file.write_all(config_str.as_bytes()).or_else(|err|
// } err!("failed to write to temp config: {}", err)
)?;
file.persist(config_path).or_else(|err|
// fn read_path_map(config: &Value) -> Result<Vec<PathPair>> { err!("failed to (over)write config: {}", err)
// if let Some(mapping) = config.get("mapping") { )?;
// if let Some(ref table) = mapping.as_table() { Ok(())
// table.iter() }
// .map(|(k, v)| Ok(PathPair{ }
// logic: k.into(),
// physic: v.as_str()
// .ok_or_else(|| Error::unknown("mapping values must be strings"))?
// .into()
// }))
// .collect()
// }
// else {
// err!("mapping must be a table, got {}", mapping)
// }
// }
// else {
// Ok(vec![])
// }
// }
pub struct Repository { pub struct Repository {
cas: Box<dyn Cas>, config: Config,
// path_map: Vec<PathPair>, cas: Box<dyn CasWithRef>,
} }
impl Repository { impl Repository {
pub fn create(path: Utf8PathBuf, config: Value) -> Result<Self> { pub fn create(path: Utf8PathBuf, config: Config) -> Result<Self> {
if path.exists() { if path.exists() {
return err!("cannot create bsv repository, path {} already exists", path); return err!("cannot create bsv repository, path {} already exists", path);
} }
let cas = create_cas(path, config)?; create_dir_all(&path)?;
config.write_toml_file(&config_path(&path))?;
let cas = SimpleCas::create(path.to_path_buf(), config.cas.clone())?;
Ok(Self { Ok(Self {
cas, config,
// path_map: vec![], cas: Box::new(cas),
}) })
} }
@ -117,39 +135,61 @@ impl Repository {
} }
let config_file = cas_simple::utils::config_path(&path); let config_file = cas_simple::utils::config_path(&path);
let config = cas_simple::utils::read_config(&config_file)?; let config = Config::from_toml_file(&config_file)?;
let cas = open_cas(path, &config)?; let cas = SimpleCas::open(path, config.cas.clone())?;
// let path_map = read_path_map(&config)?;
Ok(Self { Ok(Self {
cas, config,
// path_map, cas: Box::new(cas),
}) })
} }
pub fn cas(&self) -> &dyn Cas { pub fn cas(&self) -> &dyn CasWithRef {
self.cas.as_ref() self.cas.as_ref()
} }
// pub fn path_pair_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<PathPair> { pub fn physic_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Option<Utf8PathBuf> {
// None self.config.path_map.physic_from_logic(&logic_path)
// } }
// pub fn path_pair_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<PathPair> { pub fn path_pair_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Option<Utf8PathBuf> {
// None self.config.path_map.logic_from_physic(&physic_path)
// } }
pub fn oid_from_logic_path<P: AsRef<Utf8Path>>(&self, logic_path: P) -> Result<ObjectId> { pub fn oid_from_logic_path<P: AsRef<Utf8Path>>(&self, _logic_path: P) -> Result<ObjectId> {
// let snapshot_oid = self.cas.get_ref(Utf8Path::new("latest"))?;
// let snapshot = self.read_snapshot(snapshot_oid)?;
err!("not implemented") err!("not implemented")
} }
pub fn oid_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<ObjectId> { pub fn oid_from_physic_path<P: AsRef<Utf8Path>>(&self, physic_path: P) -> Result<ObjectId> {
err!("not implemented") let logic_path = self.config.path_map.logic_from_physic(physic_path)
.ok_or_else(|| Error::unknown("physic path do not map to a logic path"))?;
self.oid_from_logic_path(logic_path)
} }
pub fn read_tree(&self, oid: &ObjectId) -> Result<Option<Vec<TreeItem>>> { pub fn read_tree(&self, oid: &ObjectId) -> Result<Option<Vec<TreeItem>>> {
err!("not implemented") match self.cas.open_object(oid) {
Ok((metadata, mut reader)) => {
if metadata.otype() != &ObjectType::new(b"tree")? {
err!("object is not a tree")
}
else {
let mut buf_read = BufReader::new(reader.as_mut());
Ok(Some(
Vec::<TreeItem>::deserialize(&mut buf_read)?
.unwrap_or_else(|| Vec::new())
))
}
},
Err(Error::ObjectDoesNotExists(_)) => {
Ok(None)
}
Err(err) => {
Err(err)
}
}
} }
// pub fn add<P, V>(&mut self, path: P, visitor: &mut V) -> Result<ObjectId> // pub fn add<P, V>(&mut self, path: P, visitor: &mut V) -> Result<ObjectId>

121
libbsv/src/tree_item.rs

@ -27,10 +27,10 @@ pub trait Serialize {
} }
pub trait Deserialize { pub trait Deserialize {
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self> fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Option<Self>>
where Self: Sized; where Self: Sized;
fn deserialize<R: BufRead>(stream: &mut R) -> Result<Self> fn deserialize<R: BufRead>(stream: &mut R) -> Result<Option<Self>>
where Self: Sized where Self: Sized
{ {
let mut buf = Vec::new(); let mut buf = Vec::new();
@ -39,6 +39,15 @@ pub trait Deserialize {
} }
pub fn is_tree_item_name_valid(name: &str) -> bool {
return
!name.contains('/') &&
!name.contains('\\') &&
!name.contains('\n') &&
!name.contains('\0');
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct TreeItem { pub struct TreeItem {
pub name: String, pub name: String,
@ -52,6 +61,10 @@ pub struct TreeItem {
impl TreeItem { impl TreeItem {
pub fn from_metadata(name: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result<Self> { pub fn from_metadata(name: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result<Self> {
if !is_tree_item_name_valid(&name) {
return err!("invalid item name {:?}", name);
}
let otype = otype_from_metadata(metadata)?; let otype = otype_from_metadata(metadata)?;
let permissions = Permissions::from_metadata(metadata)?; let permissions = Permissions::from_metadata(metadata)?;
@ -70,7 +83,7 @@ impl TreeItem {
impl Serialize for TreeItem { impl Serialize for TreeItem {
fn serialize<W: Write>(&self, out: &mut W) -> Result<()> { fn serialize<W: Write>(&self, out: &mut W) -> Result<()> {
// TODO: Check that name do not contain invalid characters // TODO: Check that name do not contain invalid characters
writeln!(out, "{}\t{}\t{}\t{}\t{}\t{}\t{}/", writeln!(out, "{}\t{}\t{}\t{}\t{}\t{}\t{}",
self.oid, self.oid,
self.otype, self.otype,
self.size, self.size,
@ -87,28 +100,39 @@ impl Serialize for TreeItem {
} }
impl Deserialize for TreeItem { impl Deserialize for TreeItem {
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Self> { fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Option<Self>> {
let oid: ObjectId = read_field_parse(stream, buf, "object ID", b'\t')?; let oid: ObjectId = if let Some(oid) = read_field_parse(stream, buf, "object ID", b'\t')? {
oid
}
else {
return Ok(None);
};
read_field(stream, buf, "object type", b'\t')?; read_field(stream, buf, "object type", b'\t')?;
let otype = ObjectType::new(buf)?; let otype = ObjectType::new(buf)?;
let size: u64 = read_field_parse(stream, buf, "object type", b'\t')?; let size: u64 = read_field_parse(stream, buf, "object type", b'\t')?
let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?; .ok_or_else(|| Error::unknown("unexpected end-of-file"))?;
let permissions: Permissions = read_field_parse(stream, buf, "permissions", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?;
let created = UNIX_EPOCH + Duration::from_millis( let created = UNIX_EPOCH + Duration::from_millis(
read_field_parse(stream, buf, "creation date", b'\t')? read_field_parse(stream, buf, "creation date", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
); );
let modified = UNIX_EPOCH + Duration::from_millis( let modified = UNIX_EPOCH + Duration::from_millis(
read_field_parse(stream, buf, "modification date", b'\t')? read_field_parse(stream, buf, "modification date", b'\t')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
); );
let name = read_field_str(stream, buf, "name", b'/')? let name = read_field_str(stream, buf, "name", b'\n')?
.ok_or_else(|| Error::unknown("unexpected end-of-file"))?
.to_string(); .to_string();
stream.read_exact(&mut buf[..1]) if name.is_empty() {
.or_else(|err| err!("failed to read new line character: {}", err))?; err!("tree item name is empty")
if buf[0] != b'\n' { }
err!("expected new line character, got {:x}", buf[0]) else if !is_tree_item_name_valid(&name) {
err!("tree item name has invalid character(s)")
} }
else { else {
Ok(TreeItem { Ok(Some(TreeItem {
name, name,
otype, otype,
size, size,
@ -116,7 +140,7 @@ impl Deserialize for TreeItem {
modified, modified,
permissions, permissions,
oid, oid,
}) }))
} }
} }
} }
@ -133,6 +157,22 @@ impl Serialize for [TreeItem] {
} }
} }
impl Deserialize for Vec<TreeItem> {
fn deserialize_with_buf<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>) -> Result<Option<Self>> {
let mut items = Vec::new();
while let Some(item) = TreeItem::deserialize_with_buf(stream, buf)? {
items.push(item);
}
Ok(if items.is_empty() {
None
}
else {
Some(items)
})
}
}
pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result<ObjectType> { pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result<ObjectType> {
let file_type = metadata.file_type(); let file_type = metadata.file_type();
@ -152,29 +192,45 @@ pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result<ObjectType> {
} }
fn read_field<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<()> { fn read_field<R: BufRead>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<bool> {
buf.clear(); buf.clear();
stream.read_until(byte, buf) stream.read_until(byte, buf)
.or_else(|err| err!("failed to read TreeItem {}: {}", field_name, err))?; .or_else(|err| err!("failed to read TreeItem {}: {}", field_name, err))?;
if buf.is_empty() {
return Ok(false)
}
buf.pop(); buf.pop();
Ok(()) Ok(true)
} }
fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec<u8>, field_name: &str, byte: u8) -> Result<&'a str> { fn read_field_str<'a, R: BufRead>(stream: &mut R, buf: &'a mut Vec<u8>, field_name: &str, byte: u8) -> Result<Option<&'a str>> {
read_field(stream, buf, field_name, byte)?; if read_field(stream, buf, field_name, byte)? {
std::str::from_utf8(buf) Ok(Some(
.or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err)) std::str::from_utf8(buf)
.or_else(|err| err!("TreeItem {} is not valid utf-8: {}", field_name, err))?
))
}
else {
Ok(None)
}
} }
fn read_field_parse<R, I>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<I> fn read_field_parse<R, I>(stream: &mut R, buf: &mut Vec<u8>, field_name: &str, byte: u8) -> Result<Option<I>>
where where
R: BufRead, R: BufRead,
I: std::str::FromStr, I: std::str::FromStr,
<I as std::str::FromStr>::Err: std::fmt::Display <I as std::str::FromStr>::Err: std::fmt::Display
{ {
let int_str = read_field_str(stream, buf, field_name, byte)?; let maybe_str = read_field_str(stream, buf, field_name, byte)?;
I::from_str(int_str) if let Some(int_str) = maybe_str {
.or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err)) Ok(Some(
I::from_str(int_str)
.or_else(|err| err!("failed to parse TreeItem {}: {}", field_name, err))?
))
}
else {
Ok(None)
}
} }
@ -196,7 +252,7 @@ mod tests {
permissions: Permissions { read: true, write: false, execute: true }, permissions: Permissions { read: true, write: false, execute: true },
oid: ObjectId::from_str("0123456789abcdef").unwrap(), oid: ObjectId::from_str("0123456789abcdef").unwrap(),
}; };
let expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes(); let expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\n".as_bytes();
let mut result = Vec::new(); let mut result = Vec::new();
item.serialize(&mut result).unwrap(); item.serialize(&mut result).unwrap();
@ -208,7 +264,7 @@ mod tests {
fn test_deserialize_tree_item() { fn test_deserialize_tree_item() {
use std::io::Cursor; use std::io::Cursor;
let item_bytes = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes(); let item_bytes = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\n".as_bytes();
let mut item_cursor = Cursor::new(item_bytes); let mut item_cursor = Cursor::new(item_bytes);
let expected = TreeItem { let expected = TreeItem {
name: "Test $¢ह€한".to_string(), name: "Test $¢ह€한".to_string(),
@ -220,19 +276,20 @@ mod tests {
oid: ObjectId::from_str("0123456789abcdef").unwrap(), oid: ObjectId::from_str("0123456789abcdef").unwrap(),
}; };
let item = TreeItem::deserialize(&mut item_cursor).unwrap(); let item = TreeItem::deserialize(&mut item_cursor).unwrap().unwrap();
assert_eq!(item, expected); assert_eq!(item, expected);
assert!(TreeItem::deserialize(&mut Cursor::new("")).unwrap().is_none());
assert!(TreeItem::deserialize(&mut Cursor::new( assert!(TreeItem::deserialize(&mut Cursor::new(
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/".as_bytes() "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한".as_bytes()
)).is_err());
assert!(TreeItem::deserialize(&mut Cursor::new(
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/bar/\n".as_bytes()
)).is_err()); )).is_err());
assert!(TreeItem::deserialize(&mut Cursor::new( assert!(TreeItem::deserialize(&mut Cursor::new(
"0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes() "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/bar\n".as_bytes()
)).is_err()); )).is_err());
// assert!(TreeItem::deserialize(&mut Cursor::new(
// "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한\t\n".as_bytes()
// )).is_err());
assert!(TreeItem::deserialize(&mut Cursor::new( assert!(TreeItem::deserialize(&mut Cursor::new(
"0123456789abcdef\ttest\t42\tr-x\t5678000\tTest $¢ह€한\t\n".as_bytes() "0123456789abcdef\ttest\t42\tr-x\t5678000\tTest $¢ह€한\t\n".as_bytes()
)).is_err()); )).is_err());

72
libbsv/src/tree_walker.rs

@ -18,10 +18,11 @@ use std::iter::Peekable;
use std::fs::read_dir; use std::fs::read_dir;
use std::vec::IntoIter; use std::vec::IntoIter;
use camino::Utf8Path; use camino::{Utf8Path, Utf8PathBuf};
use cas_core::{Error, ObjectId, Result}; use cas_core::{Error, ObjectId, Result};
use crate::{IgnoreAction, IgnoreRules};
use crate::TreeItem; use crate::TreeItem;
@ -35,13 +36,19 @@ pub enum Action {
} }
pub struct TreeWalker { pub struct TreeWalker {
path: Utf8PathBuf,
dir_it: Peekable<IntoIter<Result<TreeItem>>>, dir_it: Peekable<IntoIter<Result<TreeItem>>>,
prev_tree_it: Peekable<IntoIter<TreeItem>>, prev_tree_it: Peekable<IntoIter<TreeItem>>,
ignore_rules: Option<IgnoreRules>,
} }
impl TreeWalker { impl TreeWalker {
pub fn new<P: AsRef<Utf8Path>>(path: P, prev_tree: Vec<TreeItem>) -> Result<Self> { pub fn new<P: AsRef<Utf8Path>>(
path: P,
prev_tree: Vec<TreeItem>,
ignore_rules: Option<IgnoreRules>
) -> Result<Self> {
let dir_entries = read_dir(path.as_ref().to_path_buf())? let dir_entries = read_dir(path.as_ref().to_path_buf())?
.map(|res| res.map_err(|err| err.into())) .map(|res| res.map_err(|err| err.into()))
.collect::<Result<Vec<_>>>()?; .collect::<Result<Vec<_>>>()?;
@ -70,10 +77,26 @@ impl TreeWalker {
}); });
Ok(Self { Ok(Self {
path: path.as_ref().to_path_buf(),
dir_it: dir_items.into_iter().peekable(), dir_it: dir_items.into_iter().peekable(),
prev_tree_it: prev_tree.into_iter().peekable(), prev_tree_it: prev_tree.into_iter().peekable(),
ignore_rules: ignore_rules,
}) })
} }
fn test_ignore(&self, item_name: &str, default_action: Action) -> Action {
if let Some(ignore_rules) = self.ignore_rules.as_ref() {
let mut path = self.path.clone();
path.push(item_name);
match ignore_rules.action_for(path) {
IgnoreAction::Accept => { default_action },
IgnoreAction::Ignore => { Action::Ignore },
}
}
else {
default_action
}
}
} }
impl Iterator for TreeWalker { impl Iterator for TreeWalker {
@ -82,7 +105,8 @@ impl Iterator for TreeWalker {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match (self.dir_it.peek(), self.prev_tree_it.peek()) { match (self.dir_it.peek(), self.prev_tree_it.peek()) {
(Some(Err(_)), _) => { (Some(Err(_)), _) => {
Some(Err(self.dir_it.next().unwrap().unwrap_err())) let error = self.dir_it.next().unwrap().unwrap_err();
Some(Err(error))
} }
(Some(Ok(curr_item)), Some(prev_item)) => { (Some(Ok(curr_item)), Some(prev_item)) => {
if curr_item.name == prev_item.name { if curr_item.name == prev_item.name {
@ -95,21 +119,28 @@ impl Iterator for TreeWalker {
else { else {
Action::Skip Action::Skip
}; };
let item = self.dir_it.next().unwrap().unwrap();
self.prev_tree_it.next().unwrap(); self.prev_tree_it.next().unwrap();
Some(Ok((action, self.dir_it.next().unwrap().unwrap()))) Some(Ok((action, item)))
} }
else if curr_item.name < prev_item.name { else if curr_item.name < prev_item.name {
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap()))) let item = self.dir_it.next().unwrap().unwrap();
let action = self.test_ignore(&item.name, Action::Add);
Some(Ok((action, item)))
} }
else { else {
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap()))) let item = self.prev_tree_it.next().unwrap();
Some(Ok((Action::Remove, item)))
} }
}, },
(Some(_), None) => { (Some(_), None) => {
Some(Ok((Action::Add, self.dir_it.next().unwrap().unwrap()))) let item = self.dir_it.next().unwrap().unwrap();
let action = self.test_ignore(&item.name, Action::Add);
Some(Ok((action, item)))
}, },
(None, Some(_)) => { (None, Some(_)) => {
Some(Ok((Action::Remove, self.prev_tree_it.next().unwrap()))) let item = self.prev_tree_it.next().unwrap();
Some(Ok((Action::Remove, item)))
}, },
(None, None) => None, (None, None) => None,
} }
@ -157,11 +188,13 @@ mod tests {
let root_dir = tempdir()?; let root_dir = tempdir()?;
let root = Utf8Path::from_path(root_dir.path()).unwrap(); let root = Utf8Path::from_path(root_dir.path()).unwrap();
let ignore_rules = IgnoreRules::from_source("*.bak", root).unwrap();
mkdir(root, "test")?; mkdir(root, "test")?;
write_file(root, "test/foobar.txt", b"baz")?; write_file(root, "test/foobar.txt", b"baz")?;
write_file(root, "readme", b"hello world!")?; write_file(root, "readme", b"hello world!")?;
let items: Vec<_> = TreeWalker::new(root, vec![]).unwrap().collect(); let items: Vec<_> = TreeWalker::new(root, vec![], Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
let (action, item) = items[0].as_ref().unwrap(); let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Add); assert_eq!(action, &Action::Add);
@ -178,7 +211,7 @@ mod tests {
write_file(root, "abc", b"xxxx")?; write_file(root, "abc", b"xxxx")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect(); let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3); assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap(); let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Add); assert_eq!(action, &Action::Add);
@ -195,7 +228,7 @@ mod tests {
remove_file(root, "readme")?; remove_file(root, "readme")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect(); let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3); assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap(); let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Skip); assert_eq!(action, &Action::Skip);
@ -210,7 +243,7 @@ mod tests {
write_file(root, "abc", b"ab")?; write_file(root, "abc", b"ab")?;
write_file(root, "test/foobar.txt", b"redacted")?; write_file(root, "test/foobar.txt", b"redacted")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items)).unwrap().collect(); let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 2); assert_eq!(items.len(), 2);
let (action, item) = items[0].as_ref().unwrap(); let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Update); assert_eq!(action, &Action::Update);
@ -220,6 +253,21 @@ mod tests {
assert_eq!(action, &Action::Skip); assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test"); assert_eq!(item.name, "test");
write_file(root, "test.bak", b"ignore this")?;
let items: Vec<_> = TreeWalker::new(root, to_tree_items(items), Some(ignore_rules.clone())).unwrap().collect();
assert_eq!(items.len(), 3);
let (action, item) = items[0].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "abc");
let (action, item) = items[1].as_ref().unwrap();
assert_eq!(action, &Action::Skip);
assert_eq!(item.name, "test");
let (action, item) = items[2].as_ref().unwrap();
assert_eq!(action, &Action::Ignore);
assert_eq!(item.name, "test.bak");
assert_eq!(item.size, 11);
Ok(()) Ok(())
} }
} }

Loading…
Cancel
Save