diff --git a/Cargo.lock b/Cargo.lock index 6b4be79..1f16536 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -26,6 +26,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bsvfs" +version = "0.1.0" +dependencies = [ + "camino", + "tempfile", + "thiserror", +] + [[package]] name = "camino" version = "1.0.7" @@ -54,6 +63,12 @@ dependencies = [ "toml", ] +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + [[package]] name = "cfg-if" version = "1.0.0" @@ -62,9 +77,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -78,31 +93,70 @@ dependencies = [ "generic-array", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", ] [[package]] -name = "getrandom" -version = "0.2.3" +name = "instant" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3" +dependencies = [ "libc", - "wasi", + "windows-sys 0.45.0", ] [[package]] name = "libbsv" version = "0.1.0" dependencies = [ + "bsvfs", "camino", "cas-core", "cas-simple", @@ -112,9 +166,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.98" +version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320cfe77175da3a483efed4bc0adc1968ca050b098ce4f2f1c13a56626128790" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" [[package]] name = "memchr" @@ -128,12 +188,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "ppv-lite86" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" - [[package]] name = "proc-macro2" version = "1.0.27" @@ -152,60 +206,20 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", - "rand_hc", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rand_hc" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" dependencies = [ "aho-corasick", "memchr", @@ -214,30 +228,35 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "rustix" +version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ - "winapi", + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", ] [[package]] name = "serde" -version = "1.0.127" +version = "1.0.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "71f2b4817415c6d4210bfe1c7bfcf4801b2d904cb4d0e1a8fdb651013c9e86b8" [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer", "cfg-if", @@ -259,16 +278,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", - "libc", - "rand", + "fastrand", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.42.0", ] [[package]] @@ -293,18 +311,18 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.8" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ "serde", ] [[package]] name = "typenum" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-xid" @@ -314,15 +332,9 @@ checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "version_check" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "winapi" @@ -345,3 +357,84 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" diff --git a/Cargo.toml b/Cargo.toml index fd9f82d..fb245c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "bsvfs", "cas-core", "cas-simple", "libbsv", diff --git a/bsvfs/Cargo.toml b/bsvfs/Cargo.toml index 5cac903..967df43 100644 --- a/bsvfs/Cargo.toml +++ b/bsvfs/Cargo.toml @@ -1,12 +1,13 @@ [package] -name = "cas-core" +name = "bsvfs" version = "0.1.0" authors = ["Simon Boyé "] edition = "2021" license = "AGPL-3.0-or-later" [dependencies] -thiserror = "1.0.25" camino = { version = "1.0.7" } +thiserror = "1.0.25" [dev-dependencies] +tempfile = "3.2.0" diff --git a/bsvfs/src/fs.rs b/bsvfs/src/fs.rs new file mode 100644 index 0000000..88fd943 --- /dev/null +++ b/bsvfs/src/fs.rs @@ -0,0 +1,491 @@ +// 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 . + +use std::{ + io::{Read, Result, Seek, Write}, + time::SystemTime, +}; + +use camino::{Utf8Path, Utf8PathBuf}; + + +// FileType + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum FileType { + Dir, + File, + Symlink, + Other, +} + +impl FileType { + pub fn is_dir(&self) -> bool { + *self == FileType::Dir + } + pub fn is_file(&self) -> bool { + *self == FileType::File + } + pub fn is_symlink(&self) -> bool { + *self == FileType::Symlink + } +} + + +// Permissions + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Permissions { + read: bool, + write: bool, + execute: bool, +} + +impl Permissions { + pub fn new(read: bool, write: bool, execute: bool) -> Self { + Permissions { read, write, execute } + } + + pub fn is_read(&self) -> bool { + self.read + } + pub fn is_write(&self) -> bool { + self.write + } + pub fn is_execute(&self) -> bool { + self.execute + } +} + +impl std::fmt::Display for Permissions { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::result::Result<(), std::fmt::Error> { + use std::fmt::Write; + + f.write_char(if self.read { 'r' } else { '-' })?; + f.write_char(if self.write { 'w' } else { '-' })?; + f.write_char(if self.execute { 'x' } else { '-' })?; + + Ok(()) + } +} + +impl std::str::FromStr for Permissions { + type Err = ParsePermissionError; + + fn from_str(s: &str) -> std::result::Result { + if s.len() == 3 { + let mut chars = s.chars(); + let read = parse_permission_char(&mut chars, 'r')?; + let write = parse_permission_char(&mut chars, 'w')?; + let execute = parse_permission_char(&mut chars, 'x')?; + assert!(chars.next().is_none()); + + Ok(Self { read, write, execute }) + } + else { + Err(ParsePermissionError::InvalidLength(s.len())) + } + } +} + +fn parse_permission_char(chars: &mut std::str::Chars, bit_char: char) + -> std::result::Result +{ + let c = chars.next().unwrap(); + if c == bit_char || c == '-' { + Ok(c == bit_char) + } + else { + Err(ParsePermissionError::InvalidChar(bit_char, c)) + } +} + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ParsePermissionError { + InvalidLength(usize), + InvalidChar(char, char), +} + +impl std::fmt::Display for ParsePermissionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::InvalidLength(length) => write!(f, "expected 3 characters, got {}", length), + Self::InvalidChar(expected, actual) => write!(f, "exected character {}, got {}", expected, actual), + } + } +} + +impl std::error::Error for ParsePermissionError { +} + + +// Metadata + +pub trait Metadata { + fn file_type(&self) -> FileType; + fn is_dir(&self) -> bool { + self.file_type().is_dir() + } + fn is_file(&self) -> bool { + self.file_type().is_file() + } + fn is_symlink(&self) -> bool { + self.file_type().is_symlink() + } + fn len(&self) -> u64; + fn permissions(&self) -> Permissions; + fn modified(&self) -> Result; + fn created(&self) -> Result; +} + + +// File + +const OPEN_READ: u8 = 0x01; +const OPEN_WRITE: u8 = 0x02; +const OPEN_APPEND: u8 = 0x04; +const OPEN_TRUNCATE: u8 = 0x08; +const OPEN_CREATE: u8 = 0x10; +const OPEN_CREATE_NEW: u8 = 0x20; + +#[derive(Debug, PartialEq, Eq)] +pub struct OpenOptions { + options: u8, +} + +impl OpenOptions { + pub fn new() -> Self { + OpenOptions { + options: 0, + } + } + + pub fn is_read(&self) -> bool { + (self.options & OPEN_READ) != 0 + } + pub fn is_write(&self) -> bool { + (self.options & OPEN_WRITE) != 0 + } + pub fn is_append(&self) -> bool { + (self.options & OPEN_APPEND) != 0 + } + pub fn is_truncate(&self) -> bool { + (self.options & OPEN_TRUNCATE) != 0 + } + pub fn is_create(&self) -> bool { + (self.options & OPEN_CREATE) != 0 + } + pub fn is_create_new(&self) -> bool { + (self.options & OPEN_CREATE_NEW) != 0 + } + + pub fn read(&mut self, read: bool) -> &mut Self { + if read { + self.options |= OPEN_READ; + } else { + self.options &= !OPEN_READ; + } + self + } + + pub fn write(&mut self, write: bool) -> &mut Self { + if write { + self.options |= OPEN_WRITE; + } else { + self.options &= !OPEN_WRITE; + } + self + } + + pub fn append(&mut self, append: bool) -> &mut Self { + if append { + self.options |= OPEN_APPEND; + } else { + self.options &= !OPEN_APPEND; + } + self + } + + pub fn truncate(&mut self, truncate: bool) -> &mut Self { + if truncate { + self.options |= OPEN_TRUNCATE; + } else { + self.options &= !OPEN_TRUNCATE; + } + self + } + + pub fn create(&mut self, create: bool) -> &mut Self { + if create { + self.options |= OPEN_CREATE; + } else { + self.options &= !OPEN_CREATE; + } + self + } + + pub fn create_new(&mut self, create_new: bool) -> &mut Self { + if create_new { + self.options |= OPEN_CREATE_NEW; + } else { + self.options &= !OPEN_CREATE_NEW; + } + self + } +} + +pub trait File: Sized + Read + Write + Seek { + type Metadata: Metadata; + + fn metadata(&self) -> Result; + + fn set_len(&self, size: u64) -> Result<()>; + + fn sync_data(&self) -> Result<()>; + fn sync_all(&self) -> Result<()>; +} + + +// DirEntry + +pub trait DirEntry { + type Metadata: Metadata; + + fn path(&self) -> Result; + fn metadata(&self) -> Result; + fn file_type(&self) -> Result { + Ok(self.metadata()?.file_type()) + } + fn file_name(&self) -> Result { + Ok( + self.path()? + .file_name() + .expect("DirEntry::path() returned an invalid path with no file name") + .to_string() + ) + } +} + + +// FileSystem + +pub trait FileSystem { + type File: File; + type DirEntry: DirEntry; + type ReadDir: Iterator>; + + fn open_with_options>(&self, path: P, options: &OpenOptions) -> Result; + + fn open>(&self, path: P) -> Result { + self.open_with_options( + path, + OpenOptions::new() + .read(true) + ) + } + fn create_or_truncate>(&self, path: P) -> Result { + self.open_with_options( + path, + OpenOptions::new() + .write(true) + .create(true) + ) + } + fn create_new>(&self, path: P) -> Result { + self.open_with_options( + path, + OpenOptions::new() + .write(true) + .create_new(true) + ) + } + + fn read_dir>(&self, path: P) -> Result; +} + + +#[cfg(test)] +mod test { + use std::str::FromStr; + + use super::*; + + #[test] + fn test_file_type() { + let dir = FileType::Dir; + assert!(dir.is_dir()); + assert!(!dir.is_file()); + assert!(!dir.is_symlink()); + + let file = FileType::File; + assert!(!file.is_dir()); + assert!(file.is_file()); + assert!(!file.is_symlink()); + + let symlink = FileType::Symlink; + assert!(!symlink.is_dir()); + assert!(!symlink.is_file()); + assert!(symlink.is_symlink()); + } + + #[test] + fn test_open_options() { + let mut options = OpenOptions::new(); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.read(true); + assert!(options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.read(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.write(true); + assert!(!options.is_read()); + assert!(options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.write(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.append(true); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.append(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.truncate(true); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.truncate(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.create(true); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(options.is_create()); + assert!(!options.is_create_new()); + + options.create(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + + options.create_new(true); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(options.is_create_new()); + + options.create_new(false); + assert!(!options.is_read()); + assert!(!options.is_write()); + assert!(!options.is_append()); + assert!(!options.is_truncate()); + assert!(!options.is_create()); + assert!(!options.is_create_new()); + } + + #[test] + fn test_permission_display() { + let expected = [ + "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx", + ]; + + for i in 0..8 { + let read = (i & 0x04) != 0; + let write = (i & 0x02) != 0; + let execute = (i & 0x01) != 0; + + let perm = Permissions { read, write, execute }; + let perm_str = perm.to_string(); + + assert_eq!(perm_str, expected[i]); + } + } + + #[test] + fn test_permission_from_str() { + let perms = [ + "---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx", + ]; + + for i in 0..8 { + let read = (i & 0x04) != 0; + let write = (i & 0x02) != 0; + let execute = (i & 0x01) != 0; + + let perm = Permissions::from_str(perms[i]).unwrap(); + let expected = Permissions { read, write, execute }; + + assert_eq!(perm, expected); + } + + assert_eq!(Permissions::from_str("rw"), Err(ParsePermissionError::InvalidLength(2))); + assert_eq!(Permissions::from_str("rw--"), Err(ParsePermissionError::InvalidLength(4))); + assert_eq!(Permissions::from_str("-x-"), Err(ParsePermissionError::InvalidChar('w', 'x'))); + assert_eq!(Permissions::from_str("123"), Err(ParsePermissionError::InvalidChar('r', '1'))); + } +} \ No newline at end of file diff --git a/bsvfs/src/fs_impl.rs b/bsvfs/src/fs_impl.rs new file mode 100644 index 0000000..00e91d8 --- /dev/null +++ b/bsvfs/src/fs_impl.rs @@ -0,0 +1,386 @@ +// 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 . + +use std::{ + error::Error, + ffi::OsString, + fmt::{Arguments, Display}, + fs::{ + self, + DirEntry as StdDirEntry, + File as StdFile, + Metadata as StdMetadata, + ReadDir as StdReadDir, + }, + io::{self, IoSlice, IoSliceMut, Read, Result, Seek, SeekFrom, Write}, + time::SystemTime, +}; + +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::{ + fs::*, +}; + + +#[derive(Clone, Debug)] +struct NonUtf8Filename { + filename: OsString, +} + +impl NonUtf8Filename { + fn new(filename: OsString) -> Self { + Self { + filename: filename, + } + } +} + +impl Display for NonUtf8Filename { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Non UTF-8 filename: '{:?}'", self.filename) + } +} + +impl Error for NonUtf8Filename { +} + + +fn as_io_error(error: E) -> io::Error + where E: Into>, +{ + io::Error::new(io::ErrorKind::Other, error) +} + +fn as_string(os_string: OsString) -> Result { + os_string.into_string() + .map_err(|filename| { + io::Error::new( + io::ErrorKind::Other, + NonUtf8Filename::new(filename), + ) + }) +} + + +fn as_filetype(std_filetype: fs::FileType) -> FileType { + if std_filetype.is_dir() { + FileType::Dir + } else if std_filetype.is_file() { + FileType::File + } else if std_filetype.is_symlink() { + FileType::Symlink + } else { + FileType::Other + } +} + +pub struct SysMetadata { + metadata: StdMetadata, +} + +impl Metadata for SysMetadata { + fn file_type(&self) -> FileType { + as_filetype(self.metadata.file_type()) + } + + fn len(&self) -> u64 { + self.metadata.len() + } + + #[cfg(not(unix))] + fn permissions(&self) -> Permissions { + Permissions(true, self.metadata.permissions().readonly(), false) + } + #[cfg(unix)] + fn permissions(&self) -> Permissions { + use std::os::unix::fs::MetadataExt; + + let mode = self.metadata.mode(); + Permissions::new( + mode & 0o100 != 0, + mode & 0o200 != 0, + mode & 0o400 != 0, + ) + } + + fn modified(&self) -> Result { + Ok(self.metadata.modified()?) + } + + fn created(&self) -> Result { + Ok(self.metadata.created()?) + } +} + +impl From for SysMetadata { + fn from(value: StdMetadata) -> Self { + SysMetadata { + metadata: value + } + } +} + + +pub struct SysFile { + file: StdFile, +} + +impl File for SysFile { + type Metadata = SysMetadata; + + fn metadata(&self) -> Result { + Ok(self.file.metadata()?.into()) + } + + fn set_len(&self, size: u64) -> Result<()> { + Ok(self.file.set_len(size)?) + } + + fn sync_data(&self) -> Result<()> { + Ok(self.file.sync_data()?) + } + + fn sync_all(&self) -> Result<()> { + Ok(self.file.sync_all()?) + } +} + +impl Read for SysFile { + fn read(&mut self, buf: &mut [u8]) -> Result { + self.file.read(buf) + } + + fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { + self.file.read_vectored(bufs) + } + // fn is_read_vectored(&self) -> bool { + // self.file.is_read_vectored() + // } + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + self.file.read_to_end(buf) + } + fn read_to_string(&mut self, buf: &mut String) -> Result { + self.file.read_to_string(buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { + self.file.read_exact(buf) + } + // fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()> { + // self.file.read_buf(buf) + // } + // fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> Result<()> { + // self.file.read_buf_exact(cursor) + // } +} + +impl Seek for SysFile { + fn seek(&mut self, pos: SeekFrom) -> Result { + self.file.seek(pos) + } + + fn rewind(&mut self) -> Result<()> { + self.file.rewind() + } + // fn stream_len(&mut self) -> Result { + // self.file.stream_len() + // } + fn stream_position(&mut self) -> Result { + self.file.stream_position() + } +} + +impl Write for SysFile { + fn write(&mut self, buf: &[u8]) -> Result { + self.file.write(buf) + } + fn flush(&mut self) -> Result<()> { + self.file.flush() + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result { + self.file.write_vectored(bufs) + } + // fn is_write_vectored(&self) -> bool { + // self.file.is_write_vectored() + // } + fn write_all(&mut self, buf: &[u8]) -> Result<()> { + self.file.write_all(buf) + } + // fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { + // self.file.write_all_vectored(bufs) + // } + fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { + self.file.write_fmt(fmt) + } +} + + +pub struct SysDirEntry { + dir_entry: StdDirEntry, +} + +impl DirEntry for SysDirEntry { + type Metadata = SysMetadata; + + fn path(&self) -> Result { + Utf8PathBuf::try_from( + self.dir_entry.path() + ).map_err(as_io_error) + } + fn metadata(&self) -> Result { + Ok(self.dir_entry.metadata()?.into()) + } + fn file_type(&self) -> Result { + Ok(as_filetype(self.dir_entry.file_type()?)) + } + fn file_name(&self) -> Result { + as_string(self.dir_entry.file_name()) + } +} + +impl From for SysDirEntry { + fn from(dir_entry: StdDirEntry) -> Self { + SysDirEntry { + dir_entry: dir_entry, + } + } +} + +pub struct SysReadDir { + read_dir: StdReadDir, +} + +impl Iterator for SysReadDir { + type Item = Result; + + fn next(&mut self) -> Option { + match self.read_dir.next() { + Some(Ok(dir_entry)) => Some(Ok(dir_entry.into())), + Some(Err(error)) => Some(Err(error)), + None => None, + } + } +} + + +pub struct SysFileSystem; + +impl SysFileSystem { + pub fn new() -> Self { + Self{} + } +} + +impl FileSystem for SysFileSystem { + type File = SysFile; + type DirEntry = SysDirEntry; + type ReadDir = SysReadDir; + + fn open_with_options>(&self, path: P, options: &OpenOptions) -> Result { + Ok(SysFile{ + file: StdFile::options() + .read(options.is_read()) + .write(options.is_write()) + .append(options.is_append()) + .truncate(options.is_truncate()) + .create(options.is_create()) + .create_new(options.is_create_new()) + .open(path.as_ref().as_std_path())?, + }) + } + + fn read_dir>(&self, path: P) -> Result { + Ok(SysReadDir { + read_dir: fs::read_dir(path.as_ref().as_std_path())?, + }) + } +} + + +#[cfg(test)] +mod test { + use camino::Utf8PathBuf; + + use super::*; + use crate::FileSystem; + + #[test] + fn test_file_read_write() { + let dir = tempfile::tempdir().expect("failed to create temp test dir"); + let file_path = Utf8PathBuf::from_path_buf(dir.path().join("test.txt")).unwrap(); + + let message = b"Hello world!\nThis is a test\n"; + + let fs = SysFileSystem::new(); + let mut file = fs.create_new(&file_path).unwrap(); + file.write(message).unwrap(); + drop(file); + + let mut file = fs.open(&file_path).unwrap(); + let mut buf = [0u8; 256]; + let read_size = file.read(&mut buf).unwrap(); + + assert_eq!(read_size, message.len()); + assert_eq!(&buf[..message.len()], message); + } + + #[test] + fn test_read_dir() { + let dir = tempfile::tempdir().expect("failed to create temp test dir"); + let dir_path = Utf8Path::from_path(dir.path()).unwrap(); + let file0_path = dir_path.join("file.txt"); + let file1_path = dir_path.join("zzz.txt"); + let dir0_path = dir_path.join("some_dir"); + + let fs = SysFileSystem::new(); + + { + let mut file = fs.create_new(file0_path).unwrap(); + file.write(b"file.txt").unwrap(); + } + { + let mut file = fs.create_new(file1_path).unwrap(); + file.write(b"zzz.txt").unwrap(); + } + fs::create_dir(dir0_path).unwrap(); + + let mut entries = fs.read_dir(dir_path).unwrap() + .collect::>>().unwrap(); + entries.sort_by_key(|entry| entry.file_name().unwrap()); + + let mut it = entries.iter(); + + let file0_entry = it.next().unwrap(); + assert_eq!(file0_entry.file_name().unwrap(), "file.txt"); + assert_eq!(file0_entry.file_type().unwrap(), FileType::File); + let file0_metadata = file0_entry.metadata().unwrap(); + assert!(file0_metadata.is_file()); + assert_eq!(file0_metadata.len(), 8); + + let dir0_entry = it.next().unwrap(); + assert_eq!(dir0_entry.file_name().unwrap(), "some_dir"); + assert_eq!(dir0_entry.file_type().unwrap(), FileType::Dir); + let dir0_metadata = dir0_entry.metadata().unwrap(); + assert!(dir0_metadata.is_dir()); + + let file1_entry = it.next().unwrap(); + assert_eq!(file1_entry.file_name().unwrap(), "zzz.txt"); + assert_eq!(file1_entry.file_type().unwrap(), FileType::File); + let file1_metadata = file1_entry.metadata().unwrap(); + assert!(file1_metadata.is_file()); + assert_eq!(file1_metadata.len(), 7); + } +} diff --git a/bsvfs/src/lib.rs b/bsvfs/src/lib.rs index e69de29..cb6080d 100644 --- a/bsvfs/src/lib.rs +++ b/bsvfs/src/lib.rs @@ -0,0 +1,32 @@ +// 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 . + +extern crate camino; +extern crate thiserror; + +#[cfg(test)] +extern crate tempfile; + + +// mod error; +mod fs; +mod fs_impl; +mod mem_fs; + + +pub use crate::{ + fs::*, + fs_impl::*, +}; diff --git a/bsvfs/src/mem_fs.rs b/bsvfs/src/mem_fs.rs new file mode 100644 index 0000000..0d4cfa8 --- /dev/null +++ b/bsvfs/src/mem_fs.rs @@ -0,0 +1,468 @@ +// 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 . + +use std::{ + fmt::{Arguments}, + io::{Cursor, Error, IoSlice, IoSliceMut, Read, Result, Seek, SeekFrom, Write, ErrorKind}, + time::SystemTime, rc::Rc, cell::RefCell, +}; + +use camino::{Utf8Path, Utf8PathBuf}; + +use crate::{ + fs::*, +}; + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MemMetadata { + file_type: FileType, + len: u64, + permissions: Permissions, + modified: SystemTime, + created: SystemTime, +} + +impl Metadata for MemMetadata { + fn file_type(&self) -> FileType { + self.file_type + } + + fn len(&self) -> u64 { + self.len + } + + fn permissions(&self) -> Permissions { + self.permissions.clone() + } + + fn modified(&self) -> Result { + Ok(self.modified) + } + + fn created(&self) -> Result { + Ok(self.created) + } +} + + +#[derive(Clone, Debug)] +pub struct MemFile { + metadata: MemMetadata, + data: RefCell>>, +} + +impl File for MemFile { + type Metadata = MemMetadata; + + // fn open_with_options>(path: P, options: &OpenOptions) -> Result { + // Ok(SysFile{ + // file: StdFile::options() + // .read(options.is_read()) + // .write(options.is_write()) + // .append(options.is_append()) + // .truncate(options.is_truncate()) + // .create(options.is_create()) + // .create_new(options.is_create_new()) + // .open(path.as_ref().as_std_path())?, + // }) + // } + + fn metadata(&self) -> Result { + Ok(self.metadata.clone()) + } + + fn set_len(&self, size: u64) -> Result<()> { + let mut data = self.data.borrow_mut(); + let pos = data.position(); + data.get_mut().resize(size as usize, 0); + if size < pos { + data.seek(SeekFrom::Start(pos)); + } + Ok(()) + } + + fn sync_data(&self) -> Result<()> { + Ok(()) + } + + fn sync_all(&self) -> Result<()> { + Ok(()) + } +} + +impl Read for MemFile { + fn read(&mut self, buf: &mut [u8]) -> Result { + let data = self.data.get_mut(); + data.read(buf) + } + + fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { + let data = self.data.get_mut(); + data.read_vectored(bufs) + } + // fn is_read_vectored(&self) -> bool { + // self.data.is_read_vectored() + // } + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + let data = self.data.get_mut(); + data.read_to_end(buf) + } + fn read_to_string(&mut self, buf: &mut String) -> Result { + let data = self.data.get_mut(); + data.read_to_string(buf) + } + fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { + let data = self.data.get_mut(); + data.read_exact(buf) + } + // fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()> { + // self.data.read_buf(buf) + // } + // fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> Result<()> { + // self.data.read_buf_exact(cursor) + // } +} + +impl Seek for MemFile { + fn seek(&mut self, pos: SeekFrom) -> Result { + let data = self.data.get_mut(); + data.seek(pos) + } + + fn rewind(&mut self) -> Result<()> { + let data = self.data.get_mut(); + data.rewind() + } + // fn stream_len(&mut self) -> Result { + // self.data.stream_len() + // } + fn stream_position(&mut self) -> Result { + let data = self.data.get_mut(); + data.stream_position() + } +} + +impl Write for MemFile { + fn write(&mut self, buf: &[u8]) -> Result { + let data = self.data.get_mut(); + data.write(buf) + } + fn flush(&mut self) -> Result<()> { + let data = self.data.get_mut(); + data.flush() + } + + fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result { + let data = self.data.get_mut(); + data.write_vectored(bufs) + } + // fn is_write_vectored(&self) -> bool { + // self.data.is_write_vectored() + // } + fn write_all(&mut self, buf: &[u8]) -> Result<()> { + let data = self.data.get_mut(); + data.write_all(buf) + } + // fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { + // self.data.write_all_vectored(bufs) + // } + fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { + let data = self.data.get_mut(); + data.write_fmt(fmt) + } +} + + +#[derive(Clone, Debug)] +struct MemDir { + metadata: MemMetadata, + items: Vec>>, +} + +impl MemDir { + fn new() -> Self { + let time = SystemTime::now(); + Self::new_with_time(&time) + } + + fn new_with_time(time: &SystemTime) -> Self { + MemDir { + metadata: MemMetadata { + file_type: FileType::Dir, + len: 0, + permissions: Permissions::new(true, true, false), + modified: time.clone(), + created: time.clone(), + }, + items: vec![], + } + } + + fn get(&self, name: &str) -> Option<&DirItem> { + None + } + fn get_mut(&self, name: &str) -> Option<&mut DirItem> { + None + } + + fn search(&self, name: &str) -> std::result::Result { + self.items.binary_search_by( + |item| item.borrow().name().cmp(name), + ) + } +} + +#[derive(Clone, Debug)] +enum DirItem { + Dir(String, MemDir), + File(String, MemFile), + Err(String, ErrorKind, String), +} + +impl DirItem { + fn name(&self) -> &str { + match self { + DirItem::Dir(ref name, _) => name, + DirItem::File(ref name, _) => name, + DirItem::Err(ref name, _, _) => name, + } + } +} + + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MemDirEntry { + path: Utf8PathBuf, + metadata: MemMetadata, +} + +impl DirEntry for MemDirEntry { + type Metadata = MemMetadata; + + fn path(&self) -> Result { + Ok(self.path.clone()) + } + fn metadata(&self) -> Result { + Ok(self.metadata.clone()) + } + fn file_type(&self) -> Result { + Ok(self.metadata.file_type()) + } + fn file_name(&self) -> Result { + Ok(self.path.file_name().unwrap().into()) + } +} + +pub struct MemReadDir { + base_path: Utf8PathBuf, + items: Vec, + index: usize, +} + +impl Iterator for MemReadDir { + type Item = Result; + + fn next(&mut self) -> Option { + match self.items.get(self.index) { + Some(DirItem::Dir(ref name, ref dir)) => { + let mut path = self.base_path.clone(); + path.push(name); + + self.index += 1; + Some(Ok(MemDirEntry{path, metadata: dir.metadata.clone()})) + }, + Some(DirItem::File(ref name, ref file)) => { + let mut path = self.base_path.clone(); + path.push(name); + + self.index += 1; + Some(Ok(MemDirEntry{path, metadata: file.metadata.clone()})) + }, + Some(DirItem::Err(_, kind, ref msg)) => { + self.index += 1; + Some(Err(Error::new(*kind, msg.clone()))) + }, + None => None, + } + } +} + + +pub struct MemFileSystem { + root: MemDir, +} + +impl MemFileSystem { + pub fn new() -> Self { + Self { + root: MemDir::new_with_time(&SystemTime::UNIX_EPOCH), + } + } + + pub fn mkdir>(&mut self, path: P, parents: bool, exist_ok: bool) -> Result<()> { + use camino::Utf8Component::*; + let mut parents = Vec::new(); + let mut iter = path.as_ref().components(); + + match iter.next() { + Some(Prefix(_)) => { + return make_err("Windows-like path are not supported by MemFileSystem"); + }, + Some(RootDir) => { + parents.push(&mut self.root); + }, + Some(_) => { + return make_err("path must be absolute"); + }, + None => { + return make_err("path is empty") + }, + } + + for comp in iter { + match comp { + Prefix(_) | + RootDir => { + panic!(); + }, + CurDir => {}, + ParentDir => { + if parents.len() <= 1 { + return make_err("invalid path") + } + parents.pop(); + }, + Normal(name) => { + let maybe_item = parents.last().unwrap().get_mut(name); + match maybe_item { + Some(DirItem::Dir(_, ref mut dir)) => { + parents.push(dir); + }, + Some(DirItem::File(_, _)) => { + return make_err("item is a file") + } + Some(DirItem::Err(_, kind, msg)) => { + return Err(std::io::Error::new(*kind, &**msg)); + } + None => {}, + } + }, + } + } + make_err("TODO") + } +} + +impl FileSystem for MemFileSystem { + type File = MemFile; + type DirEntry = MemDirEntry; + type ReadDir = MemReadDir; + + fn open_with_options>(&self, path: P, options: &OpenOptions) -> Result { + make_err("TODO") + } + + fn read_dir>(&self, path: P) -> Result { + Ok(MemReadDir { + base_path: path.as_ref().into(), + items: vec![], // TODO + index: 0, + }) + } +} + + +fn make_error(message: &str) -> std::io::Error { + std::io::Error::new( + std::io::ErrorKind::Other, + message, + ) +} +fn make_err(message: &str) -> Result { + Err(make_error(message)) +} + + +#[cfg(test)] +mod test { + // use camino::Utf8PathBuf; + + // use super::*; + + // #[test] + // fn test_file_read_write() { + // let dir = tempfile::tempdir().expect("failed to create temp test dir"); + // let file_path = Utf8PathBuf::from_path_buf(dir.path().join("test.txt")).unwrap(); + + // let message = b"Hello world!\nThis is a test\n"; + + // let mut file = SysFile::create_new(&file_path).unwrap(); + // file.write(message).unwrap(); + // drop(file); + + // let mut file = SysFile::open(&file_path).unwrap(); + // let mut buf = [0u8; 256]; + // let read_size = file.read(&mut buf).unwrap(); + + // assert_eq!(read_size, message.len()); + // assert_eq!(&buf[..message.len()], message); + // } + + // #[test] + // fn test_read_dir() { + // let dir = tempfile::tempdir().expect("failed to create temp test dir"); + // let dir_path = Utf8Path::from_path(dir.path()).unwrap(); + // let file0_path = dir_path.join("file.txt"); + // let file1_path = dir_path.join("zzz.txt"); + // let dir0_path = dir_path.join("some_dir"); + + // { + // let mut file = SysFile::create_new(file0_path).unwrap(); + // file.write(b"file.txt").unwrap(); + // } + // { + // let mut file = SysFile::create_new(file1_path).unwrap(); + // file.write(b"zzz.txt").unwrap(); + // } + // fs::create_dir(dir0_path).unwrap(); + + // let mut entries = SysFileSystem::read_dir(dir_path).unwrap() + // .collect::>>().unwrap(); + // entries.sort_by_key(|entry| entry.file_name().unwrap()); + + // let mut it = entries.iter(); + + // let file0_entry = it.next().unwrap(); + // assert_eq!(file0_entry.file_name().unwrap(), "file.txt"); + // assert_eq!(file0_entry.file_type().unwrap(), FileType::File); + // let file0_metadata = file0_entry.metadata().unwrap(); + // assert!(file0_metadata.is_file()); + // assert_eq!(file0_metadata.len(), 8); + + // let dir0_entry = it.next().unwrap(); + // assert_eq!(dir0_entry.file_name().unwrap(), "some_dir"); + // assert_eq!(dir0_entry.file_type().unwrap(), FileType::Dir); + // let dir0_metadata = dir0_entry.metadata().unwrap(); + // assert!(dir0_metadata.is_dir()); + + // let file1_entry = it.next().unwrap(); + // assert_eq!(file1_entry.file_name().unwrap(), "zzz.txt"); + // assert_eq!(file1_entry.file_type().unwrap(), FileType::File); + // let file1_metadata = file1_entry.metadata().unwrap(); + // assert!(file1_metadata.is_file()); + // assert_eq!(file1_metadata.len(), 7); + // } +} diff --git a/libbsv/Cargo.toml b/libbsv/Cargo.toml index b226b18..1d50057 100644 --- a/libbsv/Cargo.toml +++ b/libbsv/Cargo.toml @@ -9,5 +9,6 @@ license = "AGPL-3.0-or-later" toml = "0.5.8" camino = "1.0.7" regex = "1.6.0" +bsvfs = { path = "../bsvfs" } cas-core = { path = "../cas-core" } cas-simple = { path = "../cas-simple" } \ No newline at end of file diff --git a/libbsv/src/lib.rs b/libbsv/src/lib.rs index 0a1207c..7ea7092 100644 --- a/libbsv/src/lib.rs +++ b/libbsv/src/lib.rs @@ -18,10 +18,11 @@ extern crate toml; extern crate camino; extern crate regex; +extern crate bsvfs; extern crate cas_core; -mod permissions; +// mod permissions; mod tree_item; mod tree_walker; // mod config; @@ -30,7 +31,7 @@ mod ignore; mod repository; -pub use crate::permissions::Permissions; +// pub use crate::permissions::Permissions; pub use crate::tree_item::{Serialize, TreeItem}; pub use crate::path_map::{PathMap, PathPair}; pub use crate::repository::{Repository}; diff --git a/libbsv/src/permissions.rs b/libbsv/src/permissions.rs index cb6adad..4aa75af 100644 --- a/libbsv/src/permissions.rs +++ b/libbsv/src/permissions.rs @@ -14,6 +14,7 @@ // along with bsv. If not, see . +use bsvfs::{Metadata}; use cas_core::{err, Error, Result}; @@ -25,24 +26,12 @@ pub struct Permissions { } impl Permissions { - #[cfg(not(unix))] - pub fn from_metadata(metadata: &std::fs::Metadata) -> Result { - if metadata.permissions().readonly() { - Ok(Permissions::READ_ONLY) - } - else { - Ok(Permission::READ_WRITE) - } - } - - #[cfg(unix)] - pub fn from_metadata(metadata: &std::fs::Metadata) -> Result { - use std::os::unix::fs::MetadataExt; - let mode = metadata.mode(); + pub fn from_metadata(metadata: &M) -> Result { + let permissions = metadata.permissions(); Ok(Self { - read: mode & 0o100 != 0, - write: mode & 0o200 != 0, - execute: mode & 0o400 != 0, + read: permissions.is_read(), + write: permissions.is_write(), + execute: permissions.is_execute(), }) } diff --git a/libbsv/src/repository.rs b/libbsv/src/repository.rs index b4d597d..e5e0c31 100644 --- a/libbsv/src/repository.rs +++ b/libbsv/src/repository.rs @@ -20,7 +20,6 @@ use toml::Value; use cas_core::{Cas, err, Error, ObjectId, Result}; use cas_simple::{SimpleCas}; -pub use crate::permissions::Permissions; pub use crate::tree_item::{Serialize, TreeItem}; diff --git a/libbsv/src/tree_item.rs b/libbsv/src/tree_item.rs index e958368..dd8edee 100644 --- a/libbsv/src/tree_item.rs +++ b/libbsv/src/tree_item.rs @@ -17,10 +17,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::io::{BufRead, Write}; +use bsvfs::{Metadata, Permissions}; use cas_core::{err, Error, ObjectId, ObjectType, Result}; -use crate::Permissions; - pub trait Serialize { fn serialize(&self, out: &mut W) -> Result<()>; @@ -51,9 +50,10 @@ pub struct TreeItem { } impl TreeItem { - pub fn from_metadata(name: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result { + // pub fn from_metadata(name: String, metadata: &std::fs::Metadata, oid: ObjectId) -> Result { + pub fn from_metadata(name: String, metadata: &M, oid: ObjectId) -> Result { let otype = otype_from_metadata(metadata)?; - let permissions = Permissions::from_metadata(metadata)?; + let permissions = metadata.permissions(); Ok(Self { name: name, @@ -134,7 +134,7 @@ impl Serialize for [TreeItem] { } -pub fn otype_from_metadata(metadata: &std::fs::Metadata) -> Result { +pub fn otype_from_metadata(metadata: &M) -> Result { let file_type = metadata.file_type(); if file_type.is_file() { @@ -193,7 +193,7 @@ mod tests { size: 42, created: UNIX_EPOCH + Duration::from_secs(1234), modified: UNIX_EPOCH + Duration::from_secs(5678), - permissions: Permissions { read: true, write: false, execute: true }, + permissions: Permissions::new(true, false, true), oid: ObjectId::from_str("0123456789abcdef").unwrap(), }; let expected = "0123456789abcdef\ttest\t42\tr-x\t1234000\t5678000\tTest $¢ह€한/\n".as_bytes(); @@ -216,7 +216,7 @@ mod tests { size: 42, created: UNIX_EPOCH + Duration::from_secs(1234), modified: UNIX_EPOCH + Duration::from_secs(5678), - permissions: Permissions { read: true, write: false, execute: true }, + permissions: Permissions::new(true, false, true), oid: ObjectId::from_str("0123456789abcdef").unwrap(), }; diff --git a/libbsv/src/tree_walker.rs b/libbsv/src/tree_walker.rs index 6c28ed6..188d7ec 100644 --- a/libbsv/src/tree_walker.rs +++ b/libbsv/src/tree_walker.rs @@ -15,14 +15,15 @@ use std::iter::Peekable; -use std::fs::{DirEntry, Metadata, ReadDir, read_dir}; +// use std::fs::{DirEntry, Metadata, ReadDir, read_dir}; use std::vec::IntoIter; -use camino::{Utf8Path, Utf8PathBuf}; +use camino::{Utf8Path}; -use cas_core::{err, Error, ObjectId, Result}; +use bsvfs::{DirEntry, FileSystem}; +use cas_core::{ObjectId, Result}; -use crate::{PathPair, Repository, TreeItem}; +use crate::{TreeItem}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -31,7 +32,7 @@ pub enum Action { Update, Remove, Skip, - Ignore, + // Ignore, } pub struct TreeWalker { @@ -41,18 +42,14 @@ pub struct TreeWalker { impl TreeWalker { - pub fn new>(path: P, prev_tree: Vec) -> Result { - let dir_entries = read_dir(path.as_ref().to_path_buf())? + pub fn new>(fs: Fs, path: P, prev_tree: Vec) -> Result { + let dir_entries = fs.read_dir(path.as_ref().to_path_buf())? .map(|res| res.map_err(|err| err.into())) .collect::>>()?; let mut dir_items: Vec<_> = dir_entries.into_iter() .map(|dir_entry| { - let file_name = dir_entry - .file_name() - .into_string() - // .or_else(|os_string| err!("non-unicode file name '{}'", os_string.to_string_lossy()))?; - .or_else(|os_string| Err(Error::NonUnicodeFileName(os_string.to_string_lossy().into())))?; + let file_name = dir_entry.file_name()?; let metadata = dir_entry.metadata()?; Ok(TreeItem::from_metadata( file_name, @@ -117,11 +114,13 @@ impl Iterator for TreeWalker { #[cfg(test)] mod tests { + use bsvfs::SysFileSystem; + use super::*; #[test] fn test_tree_walker() { - for item in TreeWalker::new("/home/draklaw/tmp", vec![]).unwrap() { + for item in TreeWalker::new(SysFileSystem::new(), "/home/draklaw/tmp", vec![]).unwrap() { match item { Ok((action, tree_item)) => println!("{:?} {:?}", action, tree_item.name), Err(err) => println!("error while iterating directory: {}", err),