summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/devices.rs18
-rw-r--r--src/devices/file.rs201
-rw-r--r--src/devices/file/bedrock_file_path.rs205
-rw-r--r--src/devices/file/buffered_file.rs44
-rw-r--r--src/devices/file/circular_path_buffer.rs45
-rw-r--r--src/devices/file/directory_child.rs35
-rw-r--r--src/devices/file/directory_entry.rs57
-rw-r--r--src/devices/file/directory_listing.rs121
-rw-r--r--src/devices/file/operations.rs6
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