summaryrefslogtreecommitdiff
path: root/src/devices/file/bedrock_file_path.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/devices/file/bedrock_file_path.rs')
-rw-r--r--src/devices/file/bedrock_file_path.rs205
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
+ }
+}