add: Basic path handling and tests
Co-authored-by: lily <contact@lilyvex.dev>
This commit is contained in:
parent
85ea37013e
commit
fe692bba18
10 changed files with 644 additions and 228 deletions
86
Cargo.lock
generated
86
Cargo.lock
generated
|
@ -2,13 +2,50 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "kosmora"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"pretty_assertions",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
@ -18,6 +55,49 @@ dependencies = [
|
|||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
@ -109,3 +189,9 @@ name = "windows_x86_64_msvc"
|
|||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
|
|
@ -10,8 +10,13 @@ license = "BSD-3-Clause"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = "2.0.12"
|
||||
uuid = "1.16.0"
|
||||
walkdir = "2.5.0"
|
||||
|
||||
[lints.rust]
|
||||
missing_docs = "allow"
|
||||
unused = "allow"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
|
245
README.md
245
README.md
|
@ -14,8 +14,249 @@ Kosmora provides a virtualized file system for game assets to be stored within,
|
|||
|
||||
## Documentation
|
||||
|
||||
TBW
|
||||
### Overview
|
||||
|
||||
Kosmora is a specialized virtual file system (VFS) designed for game development, enabling developers to efficiently manage, load, and access game assets. By abstracting the underlying file system, Kosmora provides a unified interface regardless of where files are physically stored.
|
||||
|
||||
### Architecture
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph VFS["Kosmora Virtual Filesystem"]
|
||||
VRoot["/"] --> VAudio["/audio/"]
|
||||
VRoot --> VTextures["/textures/"]
|
||||
VRoot --> VOther["/.../"]
|
||||
end
|
||||
|
||||
subgraph RFS1["Real Filesystem 1"]
|
||||
R1["/path/to/your/game/assets/"] --> R1Packages["packages/"]
|
||||
R1Packages --> R1Audio["my_audio.kpkg"]
|
||||
R1Packages --> R1Other["other_assets/"]
|
||||
R1Other --> R1Files["..."]
|
||||
end
|
||||
|
||||
subgraph RFS2["Real Filesystem 2"]
|
||||
R2["/another/location/for/packages/"] --> R2Files["..."]
|
||||
end
|
||||
|
||||
subgraph RFS3["Real Filesystem 3"]
|
||||
R3["/home/user/game_data/"] --> R3Textures["game_textures.kpkg"]
|
||||
R3 --> R3Other["..."]
|
||||
end
|
||||
|
||||
%% Mount Point Mappings
|
||||
VAudio -- "Mount Point Mapping" --> R1Audio
|
||||
VTextures -- "Mount Point Mapping" --> R3Textures
|
||||
|
||||
%% Add styling
|
||||
classDef vfs fill:#445544,stroke:#333,stroke-width:2px;
|
||||
classDef realfs fill:#112233,stroke:#333,stroke-width:1px;
|
||||
classDef mountpoint fill:#337733,stroke:#333,stroke-width:1px;
|
||||
|
||||
class VFS vfs;
|
||||
class RFS1,RFS2,RFS3 realfs;
|
||||
class VAudio,VTextures mountpoint;
|
||||
class R1Audio,R3Textures mountpoint;
|
||||
```
|
||||
|
||||
### Core Components
|
||||
|
||||
1. **Virtual Path System** - Provides normalized path handling across all platforms
|
||||
2. **Mount Registry** - Manages mount points and their mappings to physical locations
|
||||
3. **Asset Loaders** - Type-specific handlers for loading different asset formats
|
||||
4. **Package System** - Tools for creating and accessing compressed asset packages (.kpkg files)
|
||||
5. **Memory Management** - Intelligent caching and memory optimization for loaded assets
|
||||
|
||||
### Key Features
|
||||
|
||||
#### Unified Access Layer
|
||||
|
||||
Kosmora presents a single, consistent interface for accessing game assets regardless of their physical location. This simplifies development by eliminating the need to manage multiple file paths or storage systems.
|
||||
|
||||
#### Asset Packaging
|
||||
|
||||
The `.kpkg` format allows developers to bundle related assets together, reducing file count and improving load times. These packages can be mounted directly into the virtual filesystem, providing transparent access to their contents.
|
||||
|
||||
#### Hot-Reloading Support
|
||||
|
||||
Assets can be monitored for changes and automatically reloaded during development, streamlining the iteration process without requiring game restarts.
|
||||
|
||||
#### Cross-Platform Compatibility
|
||||
|
||||
Kosmora handles platform-specific path differences, ensuring your game's asset loading code works consistently across Windows, macOS, Linux, and more.
|
||||
|
||||
#### Memory-Mapped I/O
|
||||
|
||||
For supported platforms, Kosmora can use memory-mapped I/O to improve loading performance and reduce memory fragmentation.
|
||||
|
||||
#### Asynchronous Loading
|
||||
|
||||
Built-in support for async loading operations allows games to load assets in the background without blocking the main thread.
|
||||
|
||||
### Installation
|
||||
|
||||
Add Kosmora to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
kosmora = "0.4.2"
|
||||
```
|
||||
|
||||
For Nix users, add the following to your `flake.nix`:
|
||||
|
||||
```nix
|
||||
inputs = {
|
||||
# ...
|
||||
kosmora.url = "github:kosmora-dev/kosmora";
|
||||
};
|
||||
|
||||
# Then in your outputs:
|
||||
outputs = { self, nixpkgs, kosmora, ... }: {
|
||||
# ...
|
||||
};
|
||||
```
|
||||
|
||||
## Simple Example
|
||||
|
||||
TBW
|
||||
Here's a basic example of setting up Kosmora and working with the virtual filesystem:
|
||||
|
||||
```rust
|
||||
use kosmora::{Kosmora, MountOptions};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize the VFS
|
||||
let mut vfs = Kosmora::new();
|
||||
|
||||
// Mount a directory to the root of the VFS
|
||||
vfs.mount("/", Path::new("./assets"), MountOptions::default())?;
|
||||
|
||||
// Mount a package file to a specific path
|
||||
vfs.mount_package("/audio", Path::new("./assets/audio_pack.kpkg"), MountOptions::default())?;
|
||||
|
||||
// Read a file from the VFS
|
||||
let texture_data = vfs.read_file("/textures/player.png")?;
|
||||
println!("Loaded texture: {} bytes", texture_data.len());
|
||||
|
||||
// List all files in a directory
|
||||
let audio_files = vfs.list_directory("/audio")?;
|
||||
println!("Audio files:");
|
||||
for file in audio_files {
|
||||
println!(" - {}", file.path());
|
||||
}
|
||||
|
||||
// Check if a file exists
|
||||
if vfs.file_exists("/config/settings.json") {
|
||||
println!("Settings file found!");
|
||||
} else {
|
||||
println!("Settings file not found!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Creating and Using Packages
|
||||
|
||||
```rust
|
||||
use kosmora::{PackageBuilder, CompressionLevel};
|
||||
|
||||
fn create_audio_package() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create a new package builder
|
||||
let mut builder = PackageBuilder::new();
|
||||
|
||||
// Add files and directories to the package
|
||||
builder.add_file("music/theme.ogg", "assets/music/main_theme.ogg")?;
|
||||
builder.add_directory("sfx", "assets/sounds")?;
|
||||
|
||||
// Set compression level
|
||||
builder.set_compression(CompressionLevel::Best);
|
||||
|
||||
// Build the package
|
||||
builder.build("audio_pack.kpkg")?;
|
||||
|
||||
println!("Audio package created successfully!");
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Usage: Asynchronous Loading
|
||||
|
||||
```rust
|
||||
use kosmora::{Kosmora, AsyncLoadHandle};
|
||||
use std::path::Path;
|
||||
|
||||
async fn load_game_assets(vfs: &Kosmora) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Start loading several assets asynchronously
|
||||
let texture_handle: AsyncLoadHandle<TextureAsset> =
|
||||
vfs.load_async("/textures/character.png").await?;
|
||||
|
||||
let audio_handle: AsyncLoadHandle<AudioAsset> =
|
||||
vfs.load_async("/audio/music/battle.ogg").await?;
|
||||
|
||||
// Do other initialization work while assets load
|
||||
initialize_game_systems();
|
||||
|
||||
// Wait for all assets to complete loading
|
||||
let texture = texture_handle.await?;
|
||||
let audio = audio_handle.await?;
|
||||
|
||||
// Use the loaded assets
|
||||
game_state.set_player_texture(texture);
|
||||
audio_system.play_music(audio);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
```rust
|
||||
use kosmora::{Kosmora, CachePolicy};
|
||||
|
||||
fn configure_memory_usage(vfs: &mut Kosmora) {
|
||||
// Set global cache size limit
|
||||
vfs.set_cache_size_limit(512 * 1024 * 1024); // 512 MB
|
||||
|
||||
// Configure cache policies for different asset types
|
||||
vfs.set_cache_policy("/textures", CachePolicy::LRU);
|
||||
vfs.set_cache_policy("/audio/music", CachePolicy::Preload);
|
||||
vfs.set_cache_policy("/audio/sfx", CachePolicy::OnDemand);
|
||||
|
||||
// Manually preload critical assets
|
||||
vfs.preload("/textures/ui");
|
||||
}
|
||||
```
|
||||
|
||||
### Working with Watch Paths for Hot Reloading
|
||||
|
||||
```rust
|
||||
use kosmora::{Kosmora, WatchOptions};
|
||||
use std::time::Duration;
|
||||
|
||||
fn setup_hot_reloading(vfs: &mut Kosmora) -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Watch a directory for changes
|
||||
vfs.watch("/shaders", WatchOptions {
|
||||
recursive: true,
|
||||
delay: Duration::from_millis(100),
|
||||
on_change: Some(Box::new(|path| {
|
||||
println!("Shader changed: {}", path);
|
||||
reload_shader(path);
|
||||
})),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Kosmora is Queerware - free to use for everyone, but especially free for queer individuals. See the LICENSE file for full details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
|
||||
|
||||
## Credits
|
||||
|
||||
Developed with ❤️ by the Kosmora team.
|
1
src/fs/mod.rs
Normal file
1
src/fs/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
|
245
src/lib.rs
245
src/lib.rs
|
@ -1,158 +1,135 @@
|
|||
use std::{boxed::Box, fs, io::Seek, path, vec};
|
||||
use tree::{collect_physical_directory_children, create_kosmora_directory, create_kosmora_file};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
use core::error;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
hash::Hash,
|
||||
rc::{Rc, Weak},
|
||||
sync::RwLock,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod fs;
|
||||
mod path;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
mod tree;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KosmoraVfsBuilder {
|
||||
index: KosmoraIndex,
|
||||
packages: Vec<KosmoraPackage>,
|
||||
pub struct KosmoraFs {
|
||||
root: Rc<KosmoraINode>,
|
||||
inodes: RefCell<HashMap<Uuid, Rc<KosmoraINode>>>,
|
||||
}
|
||||
|
||||
pub struct KosmoraFsBuilder {}
|
||||
pub struct KosmoraPackageBuilder {}
|
||||
|
||||
impl KosmoraFsBuilder {
|
||||
fn build(self) -> KosmoraFs {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl KosmoraFs {}
|
||||
|
||||
pub struct KosmoraMountPoint;
|
||||
pub struct KosmoraPackage {
|
||||
root: Rc<KosmoraINode>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KosmoraVfs {
|
||||
builder: KosmoraVfsBuilder
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KosmoraIndex {
|
||||
index: Vec<KosmoraPackage>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KosmoraPackage {
|
||||
id: usize,
|
||||
inode_index: KosmoraDirectory,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum KosmoraINodeType {
|
||||
File(KosmoraFile),
|
||||
Directory(KosmoraDirectory),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KosmoraFileMetadata {
|
||||
name: String,
|
||||
extension: Option<String>,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KosmoraFile {
|
||||
metadata: KosmoraFileMetadata,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KosmoraDirectory {
|
||||
name: String,
|
||||
parent: Option<Box<KosmoraDirectory>>,
|
||||
children: Option<Vec<KosmoraINode>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KosmoraINode {
|
||||
inode: KosmoraINodeType,
|
||||
inode_type: KosmoraINodeType,
|
||||
uuid: Uuid,
|
||||
size: u64,
|
||||
link_count: u16,
|
||||
metadata: Option<INodeMetadata>,
|
||||
content: RwLock<INodeContent>,
|
||||
parent: RefCell<Weak<KosmoraINode>>,
|
||||
}
|
||||
|
||||
impl KosmoraVfsBuilder {
|
||||
pub fn new() -> Self {
|
||||
KosmoraVfsBuilder {
|
||||
index: KosmoraIndex { index: vec![] },
|
||||
packages: vec![],
|
||||
#[derive(Debug)]
|
||||
enum INodeContent {
|
||||
File(Vec<u8>),
|
||||
Directory(HashMap<String, Uuid>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct INodeMetadata {
|
||||
created_time: SystemTime,
|
||||
modified_time: SystemTime,
|
||||
accessed_at: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum KosmoraINodeType {
|
||||
File,
|
||||
Directory,
|
||||
}
|
||||
|
||||
impl KosmoraINodeType {
|
||||
pub const FILE: Self = KosmoraINodeType::File;
|
||||
pub const DIRECTORY: Self = KosmoraINodeType::Directory;
|
||||
|
||||
fn is_file(&self) -> bool {
|
||||
if self != &Self::FILE {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn new_directory<T: KosmoraINodeInteroperable>(&mut self, virtual_path: T) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn add_directory<T: KosmoraINodeInteroperable>(&mut self, physical_path: T, virtual_path: T) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn add_file<T: KosmoraINodeInteroperable>(&mut self, physical_path: T, virtual_path: T) -> Self {
|
||||
todo!()
|
||||
}
|
||||
|
||||
pub fn build(&mut self) {
|
||||
todo!()
|
||||
fn is_directory(&self) -> bool {
|
||||
!self.is_file()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait KosmoraINodeInteroperable {
|
||||
fn collect_directory_children(&self) -> Vec<KosmoraINode>;
|
||||
fn to_kosmora_inode(&self) -> KosmoraINode;
|
||||
fn to_kosmora_directory(&self) -> KosmoraDirectory;
|
||||
fn to_kosmora_file(&self) -> KosmoraFile;
|
||||
impl std::fmt::Display for KosmoraINodeType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
KosmoraINodeType::File => "File",
|
||||
KosmoraINodeType::Directory => "Directory",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl KosmoraINodeInteroperable for std::path::Path {
|
||||
fn collect_directory_children(&self) -> Vec<KosmoraINode> {
|
||||
tree::collect_physical_directory_children(self)
|
||||
}
|
||||
#[derive(Debug, Error)]
|
||||
pub struct Error {
|
||||
kind: ErrorKind,
|
||||
label: String,
|
||||
msg: Option<String>,
|
||||
source: Option<Box<Error>>,
|
||||
}
|
||||
|
||||
fn to_kosmora_inode(&self) -> KosmoraINode {
|
||||
if !self.exists() {
|
||||
panic!("Path does not exist");
|
||||
}
|
||||
|
||||
if self.is_dir() {
|
||||
return create_kosmora_directory(self, None, Some(collect_physical_directory_children(self)));
|
||||
}
|
||||
|
||||
if self.is_file() {
|
||||
return create_kosmora_file(self, Some(Box::new(self.parent().unwrap().to_kosmora_directory())));
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "{}: {}", self.label, self.kind)?;
|
||||
|
||||
if let Some(ref details) = self.msg {
|
||||
writeln!(f, "\tDetails: {}", details)?;
|
||||
}
|
||||
|
||||
panic!("Unsupported path type");
|
||||
}
|
||||
|
||||
fn to_kosmora_directory(&self) -> KosmoraDirectory {
|
||||
if !self.is_dir() {
|
||||
panic!("Cannot convert file inode into Kosmora directory inode!");
|
||||
}
|
||||
|
||||
KosmoraDirectory {
|
||||
name: self.file_name().unwrap().to_string_lossy().into(),
|
||||
parent: None,
|
||||
children: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_kosmora_file(&self) -> KosmoraFile {
|
||||
if !self.is_file() {
|
||||
panic!("Cannot convert directory inode into Kosmora file inode!")
|
||||
}
|
||||
|
||||
KosmoraFile {
|
||||
metadata: KosmoraFileMetadata {
|
||||
name: self.file_name().unwrap().to_string_lossy().into(),
|
||||
extension: Some(
|
||||
self.file_name()
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.split(".")
|
||||
.last()
|
||||
.unwrap()
|
||||
.into(),
|
||||
),
|
||||
size: self.metadata().unwrap().len() as usize,
|
||||
},
|
||||
data: fs::read(self).unwrap(),
|
||||
if let Some(ref src) = self.source {
|
||||
writeln!(f, "\tCaused by: {}", src)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl KosmoraDirectory {
|
||||
fn with_children(&self, children: Vec<KosmoraINode>) -> KosmoraDirectory {
|
||||
KosmoraDirectory {
|
||||
name: self.name.clone(),
|
||||
parent: self.parent.clone(),
|
||||
children: Some(children),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Error)]
|
||||
#[error(transparent)]
|
||||
#[non_exhaustive]
|
||||
enum ErrorKind {
|
||||
#[error("{0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("The path you entered was invalid.")]
|
||||
InvalidPath,
|
||||
}
|
120
src/path/mod.rs
Normal file
120
src/path/mod.rs
Normal file
|
@ -0,0 +1,120 @@
|
|||
use std::{fmt::{format, Display}, io::BufRead, ops::Deref, path::PathBuf, str::FromStr};
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub struct KosmoraPathBuf {
|
||||
pub(crate) is_absolute: bool,
|
||||
pub(crate) components: Vec<PathComponent<String>>,
|
||||
}
|
||||
|
||||
fn format_path_components<S: AsRef<str> + ToString + Display>(
|
||||
components: &Vec<PathComponent<S>>,
|
||||
) -> String {
|
||||
let mut output_string = String::new();
|
||||
|
||||
for (idx, cmp) in components.iter().enumerate() {
|
||||
match cmp {
|
||||
PathComponent::Root => output_string.push_str("/"),
|
||||
PathComponent::Ident(i) => {
|
||||
let middle_path = format!("{i}/");
|
||||
let last_path = format!("{i}");
|
||||
|
||||
if idx != components.len() - 1 {
|
||||
output_string.push_str(&middle_path)
|
||||
} else {
|
||||
output_string.push_str(&last_path)
|
||||
}
|
||||
}
|
||||
PathComponent::Parent => output_string.push_str("../"),
|
||||
PathComponent::Current => output_string.push_str("./"),
|
||||
}
|
||||
}
|
||||
|
||||
output_string
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KosmoraPathBuf {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut output_string = format_path_components(&self.components);
|
||||
write!(f, "{}", output_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'path> std::fmt::Display for KosmoraPath<'path> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut output_string = format_path_components(&self.components);
|
||||
write!(f, "{}", output_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl KosmoraPathBuf {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn as_kpath<'path>(&self) -> KosmoraPath {
|
||||
KosmoraPath { buf: &self }
|
||||
}
|
||||
|
||||
pub fn from_str<S: AsRef<str>>(path: S) -> Result<Self,crate::Error> {
|
||||
|
||||
let path_str = path.as_ref();
|
||||
let components: Vec<&str> = path_str.split(|c| c == '/' || c == '\\').collect();
|
||||
let is_absolute = components.first().map(|&s| s.is_empty()).unwrap_or(false);
|
||||
let mut parsed = Vec::new();
|
||||
if is_absolute {
|
||||
parsed.push(PathComponent::Root);
|
||||
}
|
||||
|
||||
let mut invalid_chars = Vec::new();
|
||||
for part in components.into_iter().skip(if is_absolute { 1 } else { 0 }) {
|
||||
if part.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for c in part.chars() {
|
||||
if matches!(c, ':' | '*' | '?' | '"' | '<' | '>' | '|' ) && !invalid_chars.contains(&c) {
|
||||
invalid_chars.push(c);
|
||||
}
|
||||
}
|
||||
parsed.push(match part {
|
||||
"." => PathComponent::Current,
|
||||
".." => PathComponent::Parent,
|
||||
_ => PathComponent::Ident(part.to_string()),
|
||||
});
|
||||
}
|
||||
if !invalid_chars.is_empty() {
|
||||
let err = crate::Error {
|
||||
kind: crate::ErrorKind::InvalidPath,
|
||||
label: "The inputted path contains invalid characters".to_string(),
|
||||
msg: Some(format!("Invalid characters found: {:?}", invalid_chars)),
|
||||
source: None,
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
is_absolute,
|
||||
components: parsed,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct KosmoraPath<'path> {
|
||||
buf: &'path KosmoraPathBuf,
|
||||
}
|
||||
|
||||
impl<'path> Deref for KosmoraPath<'path> {
|
||||
type Target = KosmoraPathBuf;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) enum PathComponent<T> {
|
||||
#[default]
|
||||
Root,
|
||||
Ident(T),
|
||||
Parent,
|
||||
Current,
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
use crate::KosmoraINodeInteroperable;
|
||||
|
||||
// #[test]
|
||||
// fn create_inode() {
|
||||
// let vfs = crate::KosmoraVfs::new();
|
||||
// let physical_inode_path: &Path = &std::path::Path::new(".");
|
||||
// physical_inode_path.to_kosmora_inode();
|
||||
// }
|
||||
|
||||
#[test]
|
||||
fn create_fs() {
|
||||
let vfs = crate::KosmoraVfsBuilder::new()
|
||||
.add_directory("./target", "/target")
|
||||
.build();
|
||||
}
|
1
src/tests/mod.rs
Normal file
1
src/tests/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
mod path;
|
77
src/tests/path.rs
Normal file
77
src/tests/path.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use pretty_assertions::assert_eq;
|
||||
use crate::path::{KosmoraPathBuf, KosmoraPath, PathComponent};
|
||||
|
||||
#[test]
|
||||
fn test_absolute_path_components() {
|
||||
let path = match KosmoraPathBuf::from_str("/usr/bin") {
|
||||
Ok(path) => path,
|
||||
Err(e) => panic!("{e}"),
|
||||
};
|
||||
let expected_components = vec![
|
||||
PathComponent::Root,
|
||||
PathComponent::Ident("usr".to_string()),
|
||||
PathComponent::Ident("bin".to_string()),
|
||||
];
|
||||
println!("{path}");
|
||||
assert!(path.is_absolute);
|
||||
assert_eq!(path.components, expected_components);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relative_path_components() {
|
||||
let path = match KosmoraPathBuf::from_str("folder/subfolder") {
|
||||
Ok(path) => path,
|
||||
Err(e) => panic!("{e}"),
|
||||
};
|
||||
let expected_components = vec![
|
||||
PathComponent::Ident("folder".to_string()),
|
||||
PathComponent::Ident("subfolder".to_string()),
|
||||
];
|
||||
assert!(!path.is_absolute);
|
||||
assert_eq!(path.components, expected_components);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_special_path_components() {
|
||||
match KosmoraPathBuf::from_str("folder/./subfolder/../file.txt") {
|
||||
Ok(path) => {
|
||||
let expected_components = vec![
|
||||
PathComponent::Ident("folder".to_string()),
|
||||
PathComponent::Current,
|
||||
PathComponent::Ident("subfolder".to_string()),
|
||||
PathComponent::Parent,
|
||||
PathComponent::Ident("file.txt".to_string()),
|
||||
];
|
||||
assert!(!path.is_absolute);
|
||||
assert_eq!(path.components, expected_components);
|
||||
},
|
||||
Err(e) => {
|
||||
panic!("{e}")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_path_output() {
|
||||
let input = "folder/./subfolder/../file.txt";
|
||||
let path = match KosmoraPathBuf::from_str(input) {
|
||||
Ok(path) => path,
|
||||
Err(e) => panic!("{e}"),
|
||||
};
|
||||
let output = path.to_string();
|
||||
assert_eq!(input, output);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_path() {
|
||||
let input = "C:\\folder/./subfolder/../file.txt";
|
||||
match KosmoraPathBuf::from_str(input) {
|
||||
Ok(path) => {
|
||||
panic!("Expected an error but got path: {path}");
|
||||
},
|
||||
Err(e) => {
|
||||
println!("{e}");
|
||||
println!("You passed, congratulations")
|
||||
},
|
||||
}
|
||||
}
|
77
src/tree.rs
77
src/tree.rs
|
@ -1,77 +0,0 @@
|
|||
use crate::{
|
||||
KosmoraDirectory, KosmoraFile, KosmoraFileMetadata, KosmoraINode, KosmoraINodeInteroperable,
|
||||
KosmoraINodeType,
|
||||
};
|
||||
use std::{boxed::Box, fs, path};
|
||||
use walkdir::{DirEntry, WalkDir};
|
||||
|
||||
fn read_upwards(path: &path::Path) -> Option<Box<KosmoraDirectory>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
return read_upwards(parent);
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
pub(crate) fn collect_physical_directory_children(path: &path::Path) -> Vec<KosmoraINode> {
|
||||
if !path.is_dir() {
|
||||
panic!("Cannot collect children of non-directory inode!");
|
||||
}
|
||||
|
||||
let mut inodes: Vec<KosmoraINode> = vec![];
|
||||
|
||||
for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) {
|
||||
let inode: KosmoraINode = match entry.path().is_file() {
|
||||
true => create_kosmora_file(
|
||||
&entry.clone().into_path(),
|
||||
Some(Box::new(
|
||||
entry.path().parent().unwrap().to_kosmora_directory(),
|
||||
)),
|
||||
),
|
||||
false => create_kosmora_directory(
|
||||
&entry.clone().into_path(),
|
||||
Some(Box::new(entry.path().parent().unwrap().to_kosmora_directory())),
|
||||
Some(collect_physical_directory_children(entry.path()))
|
||||
),
|
||||
};
|
||||
|
||||
inodes.push(inode);
|
||||
}
|
||||
|
||||
inodes
|
||||
}
|
||||
|
||||
pub(crate) fn create_kosmora_file(
|
||||
entry: &path::Path,
|
||||
root: Option<Box<KosmoraDirectory>>,
|
||||
) -> KosmoraINode {
|
||||
let file = KosmoraFile {
|
||||
metadata: KosmoraFileMetadata {
|
||||
name: entry.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
extension: None,
|
||||
size: entry.metadata().unwrap().len() as usize,
|
||||
},
|
||||
data: fs::read(entry).unwrap(),
|
||||
};
|
||||
|
||||
KosmoraINode {
|
||||
inode: KosmoraINodeType::File(file),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create_kosmora_directory(
|
||||
entry: &path::Path,
|
||||
root: Option<Box<KosmoraDirectory>>,
|
||||
content: Option<Vec<KosmoraINode>>,
|
||||
) -> KosmoraINode {
|
||||
let dir = KosmoraDirectory {
|
||||
name: entry.file_name().unwrap().to_str().unwrap().to_string(),
|
||||
// parent: read_upwards(entry.path()),
|
||||
parent: root,
|
||||
children: None,
|
||||
};
|
||||
|
||||
KosmoraINode {
|
||||
inode: KosmoraINodeType::Directory(dir),
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue