diff options
-rw-r--r-- | src/devices.rs | 18 | ||||
-rw-r--r-- | src/devices/file.rs | 201 | ||||
-rw-r--r-- | src/devices/file/bedrock_file_path.rs | 205 | ||||
-rw-r--r-- | src/devices/file/buffered_file.rs | 44 | ||||
-rw-r--r-- | src/devices/file/circular_path_buffer.rs | 45 | ||||
-rw-r--r-- | src/devices/file/directory_child.rs | 35 | ||||
-rw-r--r-- | src/devices/file/directory_entry.rs | 57 | ||||
-rw-r--r-- | src/devices/file/directory_listing.rs | 121 | ||||
-rw-r--r-- | src/devices/file/operations.rs | 6 |
9 files changed, 423 insertions, 309 deletions
diff --git a/src/devices.rs b/src/devices.rs index 9df5eb2..1d69cc7 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -204,7 +204,7 @@ impl DeviceBus for StandardDevices { // 0x8F => todo!(), // File 0x90 => read_b!(self.file.entry.is_some()), - 0x91 => read_b!(self.file.op_success), + 0x91 => read_b!(self.file.success), 0x92 => self.file.name_buffer.read_byte(), 0x93 => read_b!(self.file.entry_type()), 0x94 => self.file.read_byte(), @@ -348,14 +348,14 @@ impl DeviceBus for StandardDevices { 0x95 => self.file.write_byte(val), 0x96 => self.file.set_child_name_pointer(val), 0x97 => self.file.descend_to_child(), - 0x98 => write_hh!(self.file.new_pointer), - 0x99 => write_hl!(self.file.new_pointer), - 0x9A => write_lh!(self.file.new_pointer), - 0x9B => { write_ll!(self.file.new_pointer); self.file.commit_pointer() }, - 0x9C => write_hh!(self.file.new_length), - 0x9D => write_hl!(self.file.new_length), - 0x9E => write_lh!(self.file.new_length), - 0x9F => { write_ll!(self.file.new_length); self.file.commit_length() }, + 0x98 => write_hh!(self.file.pointer), + 0x99 => write_hl!(self.file.pointer), + 0x9A => write_lh!(self.file.pointer), + 0x9B => { write_ll!(self.file.pointer); self.file.commit_pointer() }, + 0x9C => write_hh!(self.file.length), + 0x9D => write_hl!(self.file.length), + 0x9E => write_lh!(self.file.length), + 0x9F => { write_ll!(self.file.length); self.file.commit_length() }, _ => unimplemented!("Writing to device port 0x{port:02x}"), }; diff --git a/src/devices/file.rs b/src/devices/file.rs index d53db0b..26e14da 100644 --- a/src/devices/file.rs +++ b/src/devices/file.rs @@ -1,42 +1,23 @@ +mod bedrock_file_path; mod buffered_file; mod circular_path_buffer; -mod directory_entry; +mod directory_child; mod directory_listing; mod entry; mod operations; +pub use bedrock_file_path::*; pub use buffered_file::*; pub use circular_path_buffer::*; -pub use directory_entry::*; +pub use directory_child::*; pub use directory_listing::*; pub use entry::*; use operations::*; -#[cfg(target_family = "unix")] -use std::os::unix::ffi::OsStrExt; -use std::fs::{OpenOptions, metadata}; use std::path::{Component, Path, PathBuf}; -fn is_blank_path(path: &Path) -> bool { - path == PathBuf::new() -} - -fn bytes_to_path(bytes: &[u8]) -> PathBuf { - #[cfg(target_family = "unix")] - let os_string: std::ffi::OsString = { - std::os::unix::ffi::OsStringExt::from_vec(Vec::from(bytes)) - }; - #[cfg(target_family = "windows")] - let os_string: std::ffi::OsString = { - let wide: Vec<u16> = bytes.iter().map(|b| *b as u16).collect(); - std::os::windows::ffi::OsStringExt::from_wide(&wide) - }; - os_string.into() -} pub struct FileDevice { - /// The path to which the file device is confined. Files and directories - /// outside of this directory cannot be accessed. pub base_path: PathBuf, pub default_path: PathBuf, @@ -44,108 +25,104 @@ pub struct FileDevice { pub move_buffer: CircularPathBuffer, pub name_buffer: CircularPathBuffer, - pub entry: Option<(Entry, PathBuf)>, + pub entry: Option<(Entry, BedrockFilePath)>, - pub op_success: bool, - pub new_pointer: u32, - pub new_length: u32, + pub success: bool, + pub pointer: u32, + pub length: u32, + pub enable_read: bool, + pub enable_write: bool, pub enable_create: bool, pub enable_move: bool, pub enable_delete: bool, - pub enable_read: bool, - pub enable_write: bool, } impl FileDevice { pub fn new() -> Self { + #[cfg(target_family = "unix")] + let default_base: PathBuf = PathBuf::from("/"); + #[cfg(target_family = "windows")] + let default_base: PathBuf = PathBuf::from(""); + Self { - base_path: PathBuf::from("/"), + base_path: default_base, default_path: match std::env::current_dir() { Ok(dir) => PathBuf::from(dir), - Err(_) => PathBuf::from("/"), + Err(_) => PathBuf::from(""), }, + open_buffer: CircularPathBuffer::new(), move_buffer: CircularPathBuffer::new(), name_buffer: CircularPathBuffer::new(), entry: None, - op_success: false, - new_pointer: 0, - new_length: 0, + success: false, + pointer: 0, + length: 0, + enable_read: true, + enable_write: true, enable_create: true, enable_move: true, enable_delete: false, - enable_read: true, - enable_write: true, } } + /// Commit any pending writes to the currently-open file. pub fn flush_entry(&mut self) { if let Some((Entry::File(buffered_file), _)) = &mut self.entry { buffered_file.flush(); } } + /// Safely close the currently-open entry, cleaning up entry variables. pub fn close_entry(&mut self) { self.open_buffer.clear(); self.move_buffer.clear(); self.name_buffer.clear(); self.flush_entry(); self.entry = None; - self.new_pointer = 0; - self.new_length = 0; + self.pointer = 0; + self.length = 0; } + /// Process a byte received from the OPEN port. pub fn write_to_open_port(&mut self, byte: u8) { - if let Some(relative_path) = self.open_buffer.push_byte(byte) { + if let Some(buffer) = self.open_buffer.push_byte(byte) { self.close_entry(); - if !is_blank_path(&relative_path) { - if let Ok(path) = self.attach_base(&relative_path) { - let _ = self.open_entry(&path); - }; + if let Some(path) = BedrockFilePath::from_buffer(buffer, &self.base_path) { + self.success = self.open_entry(path).is_ok(); } } } - pub fn set_name_pointer(&mut self, byte: u8) { - self.name_buffer.set_pointer(byte); - } - - /// Open the entry at the given path. - pub fn open_entry(&mut self, path: &Path) -> Result<(), ()> { - macro_rules! raise_on_err { - ($res:expr) => {match $res {Ok(v)=>v, Err(_)=>return Err(())} } } - - if !path.starts_with(&self.base_path) { return Err(()); } - let metadata = raise_on_err!(metadata(&path)); + /// Opens the entry at the given path. + pub fn open_entry(&mut self, path: BedrockFilePath) -> Result<(), ()> { + macro_rules! unres { + ($result:expr) => { match $result {Ok(v)=>v,Err(_)=>return Err(())} }; + } + let absolute_path = path.as_path(); + let metadata = unres!(std::fs::metadata(&absolute_path)); if metadata.is_file() { - let open_result = OpenOptions::new() + let open_result = std::fs::OpenOptions::new() .read(self.enable_read) .write(self.enable_write) - .open(&path); + .open(&absolute_path); + // Keep the current entry open if we can't open the new path. if let Ok(file) = open_result { self.close_entry(); - let file_entry = Entry::File(BufferedFile::new(file)); - self.entry = Some((file_entry, path.to_owned())); - let relative = remove_base(&path, &self.base_path).unwrap(); - #[cfg(target_family = "unix")] - self.name_buffer.populate(relative.as_os_str().as_bytes()); - #[cfg(target_family = "windows")] - self.name_buffer.populate(relative.as_os_str().as_encoded_bytes()); + self.name_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::File(BufferedFile::new(file)), path)); return Ok(()); }; } else if metadata.is_dir() { - if let Ok(listing) = DirectoryListing::from_path(&path, &self.base_path) { - let dir_entry = Entry::Directory(listing); - self.entry = Some((dir_entry, path.to_owned())); - let relative = remove_base(&path, &self.base_path).unwrap(); - #[cfg(target_family = "unix")] - self.name_buffer.populate(relative.as_os_str().as_bytes()); - #[cfg(target_family = "windows")] - self.name_buffer.populate(relative.as_os_str().as_encoded_bytes()); + // Keep the current entry open if we can't open the new path. + if let Some(listing) = path.directory_listing() { + self.close_entry(); + self.name_buffer.populate(path.as_buffer()); + self.entry = Some((Entry::Directory(listing), path)); return Ok(()); }; }; @@ -153,58 +130,60 @@ impl FileDevice { } pub fn write_to_move_port(&mut self, byte: u8) { - if let Some(dest) = self.move_buffer.push_byte(byte) { + if let Some(buffer) = self.move_buffer.push_byte(byte) { + let blank_destination = buffer[0] == 0x00; + let destination = BedrockFilePath::from_buffer(buffer, &self.base_path); + self.success = false; + if let Some((_, source)) = &self.entry { - if is_blank_path(&dest) { - match self.enable_delete { - true => self.op_success = delete_entry(&source), - false => self.op_success = false, + if blank_destination { + if self.enable_delete { + self.success = delete_entry(&source.as_path()); } - } else if let Ok(destination) = self.attach_base(&dest) { - match self.enable_move { - true => self.op_success = move_entry(&source, &destination), - false => self.op_success = false, + } else if let Some(dest) = destination { + if self.enable_move { + self.success = move_entry(&source.as_path(), &dest.as_path()); } } - } else { - if is_blank_path(&dest) { - self.op_success = false; - } else if let Ok(destination) = self.attach_base(&dest) { - match self.enable_create { - true => self.op_success = create_file(&destination), - false => self.op_success = false, - } + } else if let Some(dest) = destination { + if self.enable_create { + self.success = create_file(&dest.as_path()); } } + self.close_entry(); } } /// Attempt to open the parent directory of the current entry. pub fn ascend_to_parent(&mut self) { + self.success = false; if let Some((_, path)) = &self.entry { if let Some(parent_path) = path.parent() { - self.op_success = self.open_entry(&parent_path.to_owned()).is_ok(); - } else { - self.op_success = false; + self.success = self.open_entry(parent_path).is_ok(); } } else { - self.op_success = self.open_entry(&self.default_path.to_owned()).is_ok(); + if let Some(default) = BedrockFilePath::from_path(&self.default_path, &self.base_path) { + self.success = self.open_entry(default).is_ok(); + } } } - /// Attempt to open the currently-selected child. + /// Attempt to open the currently-selected child of the current directory. pub fn descend_to_child(&mut self) { + self.success = false; if let Some((Entry::Directory(listing), _)) = &self.entry { if let Some(child_path) = listing.child_path() { - if let Ok(child_path) = self.attach_base(&child_path) { - self.op_success = self.open_entry(&child_path).is_ok(); - } + self.success = self.open_entry(child_path).is_ok(); }; } } - /// Return true if the currently-open entry is a directory. + pub fn set_name_pointer(&mut self, value: u8) { + self.name_buffer.set_pointer(value); + } + + /// Returns true if the currently-open entry is a directory. pub fn entry_type(&self) -> bool { match self.entry { Some((Entry::Directory(_), _)) => true, @@ -212,7 +191,7 @@ impl FileDevice { } } - /// Return true if the currently-selected child is a directory. + /// Reads a byte from the name buffer of the currently-selected child. pub fn read_child_name(&mut self) -> u8 { if let Some((Entry::Directory(listing), _)) = &mut self.entry { listing.child_name().read_byte() @@ -227,7 +206,7 @@ impl FileDevice { } } - /// Return true if the currently-selected child is a directory. + /// Returns true if the currently-selected child is a directory. pub fn child_type(&self) -> bool { if let Some((Entry::Directory(listing), _)) = &self.entry { match listing.child_type() { @@ -240,6 +219,7 @@ impl FileDevice { } } + /// Reads a byte from the currently-open file. pub fn read_byte(&mut self) -> u8 { match &mut self.entry { Some((Entry::File(buffered_file), _)) => buffered_file.read_byte(), @@ -247,6 +227,7 @@ impl FileDevice { } } + /// Writes a byte to the currently-open file. pub fn write_byte(&mut self, byte: u8) { match &mut self.entry { Some((Entry::File(buffered_file), _)) => buffered_file.write_byte(byte), @@ -263,7 +244,7 @@ impl FileDevice { } pub fn commit_pointer(&mut self) { - let pointer = std::mem::take(&mut self.new_pointer); + let pointer = std::mem::take(&mut self.pointer); match &mut self.entry { Some((Entry::File(buffered_file), _)) => buffered_file.set_pointer(pointer), Some((Entry::Directory(listing), _)) => listing.set_selected(pointer), @@ -280,30 +261,12 @@ impl FileDevice { } pub fn commit_length(&mut self) { - let length = std::mem::take(&mut self.new_length); + let length = std::mem::take(&mut self.length); match &mut self.entry { Some((Entry::File(buffered_file), _)) => buffered_file.set_length(length), _ => (), } } - - fn attach_base(&self, relative_path: &Path) -> Result<PathBuf, ()> { - let mut full_path = self.base_path.clone(); - let mut has_root = false; - for component in relative_path.components() { - match component { - Component::Normal(s) => full_path.push(s), - Component::ParentDir => return Err(()), - Component::CurDir => continue, - Component::RootDir => has_root = true, - Component::Prefix(_) => continue, - } - }; - match has_root { - true => Ok(full_path), - false => Err(()) - } - } } impl Drop for FileDevice { diff --git a/src/devices/file/bedrock_file_path.rs b/src/devices/file/bedrock_file_path.rs new file mode 100644 index 0000000..c169a62 --- /dev/null +++ b/src/devices/file/bedrock_file_path.rs @@ -0,0 +1,205 @@ +use super::*; + +use std::cmp::Ordering; +use std::ffi::OsString; + +#[cfg(target_family = "unix")] +use std::os::unix::ffi::OsStringExt; +#[cfg(target_family = "windows")] +use std::os::windows::ffi::OsStringExt; + + +#[derive(Clone)] +pub struct BedrockFilePath { + base: PathBuf, + relative: PathBuf, + bytes: Vec<u8>, +} + +impl BedrockFilePath { + pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option<Self> { + let base = base.to_path_buf(); + let relative = buffer_to_path(buffer)?; + let bytes = path_to_bytes(&relative)?; + assert_path_is_safe(&relative)?; + Some(Self { base, relative, bytes }) + } + + /// Construct an instance from an absolute path and a prefix of that path. + pub fn from_path(path: &Path, base: &Path) -> Option<Self> { + let base = base.to_path_buf(); + let relative = path.strip_prefix(&base).ok()?.to_path_buf(); + let bytes = path_to_bytes(&relative)?; + assert_path_is_safe(&relative)?; + Some( Self { base, relative, bytes } ) + } + + /// Get the base path used by this path. + pub fn base(&self) -> &Path { + &self.base + } + + /// Get this path as a Bedrock-style path, which can be passed to + /// a Bedrock program. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes + } + + /// Get this path as a byte buffer, from which a CircularPathBuffer + /// can be populated. + pub fn as_buffer(&self) -> [u8; 256] { + let mut buffer: [u8; 256] = [0; 256]; + buffer[..self.bytes.len()].copy_from_slice(&self.bytes); + return buffer; + } + + /// Get this path as an absolute operating-system path, which can + /// be used to open a file or directory. + pub fn as_path(&self) -> PathBuf { + self.base.join(&self.relative) + } + + /// Get a path which represents the parent of this path. + pub fn parent(&self) -> Option<Self> { + let relative = self.relative.parent()?.to_path_buf(); + let base = self.base.clone(); + let bytes = path_to_bytes(&relative)?; + Some( Self { base, relative, bytes } ) + } + + pub fn directory_listing(&self) -> Option<DirectoryListing> { + DirectoryListing::from_path(&self) + } + + /// Returns true if a dot character directly follows the final + /// forward-slash character in the relative path. + pub fn filename_is_dot_prefixed(&self) -> bool { + let bytes = self.as_bytes(); + let mut dot_i = 0; + for (i, b) in bytes.iter().enumerate() { + if *b == b'/' { + // Guaranteed to be a valid index, bytes is null-terminated. + dot_i = i + 1; + } else if *b == 0x00 { + break; + } + } + return bytes[dot_i] == b'.'; + } +} + + +/// Converts the contents of a CircularPathBuffer to a relative path. +fn buffer_to_path(bytes: [u8; 256]) -> Option<PathBuf> { + // The buffer must be non-empty and slash-prefixed. + if bytes[0] != ('/' as u8) { + return None; + } + + // Find the index of the first null byte. + let mut null_i = None; + for (i, b) in bytes.iter().enumerate() { + if *b == 0x00 { + null_i = Some(i); + break; + } + } + // Take a slice, excluding the leading slash, up to the trailing null. + let slice = &bytes[1..null_i?]; + + #[cfg(target_family = "unix")] { + let vec = Vec::from(slice); + return Some(OsString::from_vec(vec).into()) + } + #[cfg(target_family = "windows")] { + let string = String::from_utf8_lossy(slice); + let utf16: Vec<u16> = string.replace(r"/", r"\").encode_utf16().collect(); + return Some(OsString::from_wide(&utf16).into()) + } +} + +/// Convert an operating system path to a Bedrock-style byte path. +/// +/// A byte path contains at most 255 bytes, and is not null-terminated. +fn path_to_bytes(path: &Path) -> Option<Vec<u8>> { + let mut string = String::from("/"); + #[cfg(target_family = "unix")] + string.push_str(path.as_os_str().to_str()?); + #[cfg(target_family = "windows")] + string.push_str(&path.as_os_str().to_str()?.replace(r"\", r"/")); + + // Remove all trailing forward-slash characters. + let slice = string.trim_end_matches('/').as_bytes(); + + // Error if bytes does not fit into a CircularPathBuffer. + if slice.len() > 255 { return None; } + + Some(Vec::from(slice)) +} + +/// Returns true if a path contains only normal components. +fn assert_path_is_safe(relative: &Path) -> Option<()> { + // Error if path contains special components. + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + return Some(()); +} + + +// --------------------------------------------------------------------------- + +impl PartialEq for BedrockFilePath { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes + } +} + +impl Eq for BedrockFilePath {} + +impl PartialOrd for BedrockFilePath { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for BedrockFilePath { + fn cmp(&self, other: &Self) -> Ordering { + compare_ascii_slices(&self.bytes, &other.bytes) + } +} + +// Compare two ASCII byte-slices in case-agnostic alphabetic order. +fn compare_ascii_slices(left: &[u8], right: &[u8]) -> Ordering { + let l = std::cmp::min(left.len(), right.len()); + let lhs = &left[..l]; + let rhs = &right[..l]; + + for i in 0..l { + let a = remap_ascii(lhs[i]); + let b = remap_ascii(rhs[i]); + match a.cmp(&b) { + Ordering::Equal => (), + non_eq => return non_eq, + } + } + + left.len().cmp(&right.len()) +} + +// Remap ASCII values so that they sort in case-agnostic alphabetic order: +// !"#$%&'()*+,-./0123456789:;<=>? +// @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo +// PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_ +fn remap_ascii(c: u8) -> u8 { + if 0x40 <= c && c <= 0x5F { + (c - 0x40) * 2 + 0x40 + } else if 0x60 <= c && c <= 0x7F { + (c - 0x60) * 2 + 0x41 + } else { + c + } +} diff --git a/src/devices/file/buffered_file.rs b/src/devices/file/buffered_file.rs index 04fefbd..73d3536 100644 --- a/src/devices/file/buffered_file.rs +++ b/src/devices/file/buffered_file.rs @@ -3,27 +3,6 @@ use std::io::{BufReader, BufWriter}; use std::io::{Read, Write}; use std::io::{ErrorKind, Seek, SeekFrom}; -enum AccessMode { - Read(BufReader<File>), - Write(BufWriter<File>), - None, -} - -impl AccessMode { - pub fn unwrap(self) -> File { - match self { - Self::Read(reader) => reader.into_inner(), - Self::Write(writer) => writer.into_inner().unwrap(), - Self::None => unreachable!(), - } - } -} - -impl Default for AccessMode { - fn default() -> Self { - Self::None - } -} pub struct BufferedFile { file: AccessMode, @@ -135,3 +114,26 @@ impl BufferedFile { }; } } + + +enum AccessMode { + Read(BufReader<File>), + Write(BufWriter<File>), + None, +} + +impl AccessMode { + pub fn unwrap(self) -> File { + match self { + Self::Read(reader) => reader.into_inner(), + Self::Write(writer) => writer.into_inner().unwrap(), + Self::None => unreachable!(), + } + } +} + +impl Default for AccessMode { + fn default() -> Self { + Self::None + } +} diff --git a/src/devices/file/circular_path_buffer.rs b/src/devices/file/circular_path_buffer.rs index e5d903b..9d1dea6 100644 --- a/src/devices/file/circular_path_buffer.rs +++ b/src/devices/file/circular_path_buffer.rs @@ -1,5 +1,3 @@ -use super::*; - pub struct CircularPathBuffer { buffer: [u8; 256], pointer: u8, @@ -10,29 +8,29 @@ impl CircularPathBuffer { Self { buffer: [0; 256] , pointer: 0 } } - pub fn clear(&mut self) { - self.buffer.fill(0); + /// Clear the buffer, returning the previous buffer contents. + pub fn clear(&mut self) -> [u8; 256] { self.pointer = 0; + std::mem::replace(&mut self.buffer, [0; 256]) } - pub fn populate(&mut self, bytes: &[u8]) { - self.clear(); - if bytes.len() > 255 { - unreachable!( - "Attempted to populate CircularPathBuffer with {} bytes: {:?}", - bytes.len(), String::from_utf8_lossy(bytes) - ) - } - let self_slice = &mut self.buffer[..bytes.len()]; - self_slice.copy_from_slice(&bytes); + /// Reset the pointer and hot-swap the byte buffer. + pub fn populate(&mut self, buffer: [u8; 256]) { + self.pointer = 0; + self.buffer = buffer; } + /// Move internal pointer to either the start of the path or the file name. + /// + /// If value is non-zero, the pointer will be moved to the byte + /// directly following the final forward-slash. pub fn set_pointer(&mut self, value: u8) { self.pointer = 0; + // Set the pointer to the start of the filename if value is truthy. if value != 0x00 { for (i, c) in self.buffer.iter().enumerate() { match c { - b'/' => self.pointer = (i + 1) as u8, + b'/' => self.pointer = (i as u8).saturating_add(1), 0x00 => break, _ => continue, } @@ -40,23 +38,22 @@ impl CircularPathBuffer { } } + /// Read a single byte from the buffer. pub fn read_byte(&mut self) -> u8 { let pointer = self.pointer as usize; self.pointer = self.pointer.wrapping_add(1); self.buffer[pointer] } - // Returns an unsanitized relative path. - pub fn push_byte(&mut self, value: u8) -> Option<PathBuf> { - if value == 0x00 { - let pointer = self.pointer as usize; - let path = bytes_to_path(&self.buffer[..pointer]); - self.clear(); - Some(path) + /// Write a single byte to the buffer. + /// + /// If a null-byte is written, the buffer will be cleared and returned. + pub fn push_byte(&mut self, byte: u8) -> Option<[u8; 256]> { + if byte == 0x00 { + Some(self.clear()) } else { - let pointer = self.pointer as usize; + self.buffer[self.pointer as usize] = byte; self.pointer = self.pointer.wrapping_add(1); - self.buffer[pointer] = value; None } } diff --git a/src/devices/file/directory_child.rs b/src/devices/file/directory_child.rs new file mode 100644 index 0000000..376ec7d --- /dev/null +++ b/src/devices/file/directory_child.rs @@ -0,0 +1,35 @@ +use super::*; + +use std::cmp::Ordering; + + +pub struct DirectoryChild { + pub path: BedrockFilePath, + pub entry_type: EntryType, +} + + +// --------------------------------------------------------------------------- + +impl PartialEq for DirectoryChild { + fn eq(&self, other: &Self) -> bool { + self.entry_type == other.entry_type && self.path == other.path + } +} + +impl Eq for DirectoryChild {} + +impl PartialOrd for DirectoryChild { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for DirectoryChild { + fn cmp(&self, other: &Self) -> Ordering { + match self.entry_type.cmp(&other.entry_type) { + Ordering::Equal => self.path.cmp(&other.path), + ordering => ordering, + } + } +} diff --git a/src/devices/file/directory_entry.rs b/src/devices/file/directory_entry.rs deleted file mode 100644 index 7f0e69b..0000000 --- a/src/devices/file/directory_entry.rs +++ /dev/null @@ -1,57 +0,0 @@ -use super::*; - -use std::cmp::Ordering; - -#[derive(PartialEq, Eq)] -pub struct DirectoryChild { - pub byte_path: Vec<u8>, - pub entry_type: EntryType, -} - -impl PartialOrd for DirectoryChild { - fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.cmp(other)) - } -} - -impl Ord for DirectoryChild { - fn cmp(&self, other: &Self) -> Ordering { - match self.entry_type.cmp(&other.entry_type) { - Ordering::Equal => - compare_ascii_arrays(&self.byte_path,&other.byte_path), - other => other, - } - } -} - -// Compare two ASCII arrays in case-agnostic alphabetic order. -fn compare_ascii_arrays(left: &[u8], right: &[u8]) -> Ordering { - let l = std::cmp::min(left.len(), right.len()); - let lhs = &left[..l]; - let rhs = &right[..l]; - - for i in 0..l { - let a = remap_ascii(lhs[i]); - let b = remap_ascii(rhs[i]); - match a.cmp(&b) { - Ordering::Equal => (), - non_eq => return non_eq, - } - } - - left.len().cmp(&right.len()) -} - -// Remap ASCII values so that they sort in case-agnostic alphabetic order: -// !"#$%&'()*+,-./0123456789:;<=>? -// @`AaBbCcDdEeFfGgHhIiJjKkLlMmNnOo -// PpQqRrSsTtUuVvWwXxYyZz[{\|]}^~_ -fn remap_ascii(c: u8) -> u8 { - if 0x40 <= c && c <= 0x5F { - (c - 0x40) * 2 + 0x40 - } else if 0x60 <= c && c <= 0x7F { - (c - 0x60) * 2 + 0x41 - } else { - c - } -} diff --git a/src/devices/file/directory_listing.rs b/src/devices/file/directory_listing.rs index 341f353..0cbbde9 100644 --- a/src/devices/file/directory_listing.rs +++ b/src/devices/file/directory_listing.rs @@ -1,5 +1,6 @@ use super::*; + pub struct DirectoryListing { children: Vec<DirectoryChild>, length: u32, @@ -8,51 +9,50 @@ pub struct DirectoryListing { } impl DirectoryListing { - pub fn from_path(path: &Path, base: &Path) -> Result<Self, ()> { - macro_rules! continue_on_err { + pub fn from_path(path: &BedrockFilePath) -> Option<Self> { + macro_rules! unres { ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} }; } + macro_rules! unopt { + ($option:expr) => { match $option { Some(v) => v, None => continue} }; + } let mut children = Vec::new(); - if let Ok(iter_dir) = std::fs::read_dir(path) { - for (i, entry_result) in iter_dir.enumerate() { - if i == u16::MAX as usize { - eprintln!("Warning, {path:?} contains more than 65536 entries.") - }; - let entry = continue_on_err!(entry_result); - let path = continue_on_err!(remove_base(&entry.path(), &base)); - #[cfg(target_family = "unix")] - let byte_path = path.as_os_str().as_bytes(); - #[cfg(target_family = "windows")] - let byte_path = path.as_os_str().as_encoded_bytes(); - if byte_path.len() > 255 { + let dir_listing = std::fs::read_dir(path.as_path()).ok()?; + for (i, entry_result) in dir_listing.enumerate() { + // Firebreak to prevent emulator from consuming an absurd amount + // of memory when opening too large of a directory. + if i == (u16::MAX as usize) { + break; + } + + let entry = unres!(entry_result); + let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base())); + if entry_path.filename_is_dot_prefixed() { + continue; + } + let metadata = unres!(std::fs::metadata(&entry.path())); + let entry_type = + if metadata.is_file() { + EntryType::File + } else if metadata.is_dir() { + EntryType::Directory + } else { continue; }; - if filename_dot_prefixed(&path) { - continue; - } - let metadata = continue_on_err!(std::fs::metadata(&entry.path())); - children.push( DirectoryChild { - byte_path: Vec::from(byte_path), - entry_type: if metadata.is_file() { - EntryType::File - } else if metadata.is_dir() { - EntryType::Directory - } else { - continue; - }, - } ) - } - children.sort_unstable(); - let length = u32::try_from(children.len()).unwrap_or(u32::MAX); - let selected = None; - let name_buffer = CircularPathBuffer::new(); - Ok(Self { children, length, selected, name_buffer } ) - } else { - Err(()) + + let child = DirectoryChild { path: entry_path, entry_type }; + children.push(child); } + + children.sort(); + let length = u32::try_from(children.len()).ok()?; + let selected = None; + let name_buffer = CircularPathBuffer::new(); + Some( Self { children, length, selected, name_buffer } ) } + /// Attempts to return a directory child by index. pub fn get(&self, index: u32) -> Option<&DirectoryChild> { self.children.get(index as usize) } @@ -61,13 +61,16 @@ impl DirectoryListing { self.length } + /// Returns the index of the selected child, or zero if no child is selected. pub fn selected(&self) -> u32 { self.selected.unwrap_or(0) } + /// Attempts to select a child by index. pub fn set_selected(&mut self, index: u32) { - if let Some(info) = self.get(index) { - self.name_buffer.populate(&info.byte_path.clone()); + if let Some(child) = self.get(index) { + let buffer = child.path.as_buffer(); + self.name_buffer.populate(buffer); self.selected = Some(index); } else { self.name_buffer.clear(); @@ -83,46 +86,8 @@ impl DirectoryListing { self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.entry_type))) } - pub fn child_path(&self) -> Option<PathBuf> { - self.selected.and_then(|s| self.get(s).and_then(|i| { - Some(bytes_to_path(&i.byte_path)) - })) - } -} - -pub fn remove_base(absolute_path: &Path, base_path: &Path) -> Result<PathBuf, ()> { - if let Ok(relative) = absolute_path.strip_prefix(base_path) { - let mut baseless_path = PathBuf::from("/"); - for component in relative.components() { - match component { - Component::Normal(s) => baseless_path.push(s), - Component::ParentDir => return Err(()), - Component::CurDir => continue, - Component::RootDir => continue, - Component::Prefix(_) => continue, - } - } - return Ok(baseless_path); + pub fn child_path(&self) -> Option<BedrockFilePath> { + self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.path.clone()))) } - return Err(()); } -// Returns true if a dot character directly follows the right-most -// forward-slash character in the path. -fn filename_dot_prefixed(path: &Path) -> bool { - #[cfg(target_family = "unix")] - let bytes = path.as_os_str().as_bytes(); - #[cfg(target_family = "windows")] - let bytes = path.as_os_str().as_encoded_bytes(); - // Find position of final forward-slash byte. - let mut final_slash = None; - for (i, byte) in bytes.iter().enumerate() { - if *byte == b'/' { final_slash = Some(i) } - } - if let Some(i) = final_slash { - if let Some(b'.') = bytes.get(i+1) { - return true; - } - } - return false; -} diff --git a/src/devices/file/operations.rs b/src/devices/file/operations.rs index f33509b..0593ac8 100644 --- a/src/devices/file/operations.rs +++ b/src/devices/file/operations.rs @@ -1,11 +1,13 @@ use std::io::ErrorKind; use std::path::Path; + +/// Returns true if an entry already exists at the given path. pub fn entry_exists(source: &Path) -> bool { std::fs::metadata(source).is_ok() } -// Delete a file or folder, returning true if successful. +/// Delete an entry, returning true if successful. pub fn delete_entry(source: &Path) -> bool { match std::fs::remove_file(source) { Ok(_) => true, @@ -23,6 +25,7 @@ pub fn delete_entry(source: &Path) -> bool { } } +/// Move an entry from one location to another, returning true if successful. pub fn move_entry(source: &Path, destination: &Path) -> bool { if !entry_exists(source) || entry_exists(destination) { return false; @@ -30,6 +33,7 @@ pub fn move_entry(source: &Path, destination: &Path) -> bool { std::fs::rename(source, destination).is_ok() } +/// Create a new file if it doesn't already exist, returning true if successful. pub fn create_file(destination: &Path) -> bool { if entry_exists(destination) { false |