diff options
Diffstat (limited to 'src/devices/file/bedrock_file_path.rs')
-rw-r--r-- | src/devices/file/bedrock_file_path.rs | 205 |
1 files changed, 205 insertions, 0 deletions
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 + } +} |