diff options
Diffstat (limited to 'src/types/file_path.rs')
-rw-r--r-- | src/types/file_path.rs | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/src/types/file_path.rs b/src/types/file_path.rs new file mode 100644 index 0000000..7e6dbe8 --- /dev/null +++ b/src/types/file_path.rs @@ -0,0 +1,288 @@ +use crate::*; + +use std::cmp::Ordering; +use std::ffi::OsString; +use std::path::Component; + + +#[derive(Clone)] +pub struct BedrockFilePath { + /// Sandbox directory + base: PathBuf, + /// Path relative to sandbox directory + relative: PathBuf, + bytes: Vec<u8>, + entry_type: Option<EntryType>, +} + +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)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some(Self { base, relative, bytes, entry_type }) + } + + /// 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)?; + let entry_type = get_entry_type(base.join(&relative)); + assert_path_is_safe(&relative, &base)?; + Some( Self { base, relative, bytes, entry_type } ) + } + + /// 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 the entry type of this path. + pub fn entry_type(&self) -> Option<EntryType> { + self.entry_type + } + + /// Get a path which represents the parent of this path. + pub fn parent(&self) -> Option<Self> { + #[cfg(target_family = "unix")] { + Self::from_path(self.as_path().parent()?, &self.base) + } + #[cfg(target_family = "windows")] { + if self.base.components().count() != 0 { + // Sandboxed path, cannot ascend to a virtual root directory. + Self::from_path(self.as_path().parent()?, &self.base) + } else { + // Unsandboxed path, we can ascend to a virtual root directory. + match self.as_path().parent() { + // Ascend to concrete parent directory. + Some(parent) => Self::from_path(parent, &self.base), + // Ascend into a virtual root directory. + None => { + if self.relative.components().count() != 0 { + // Ascend from concrete path to virtual root. + let blank = PathBuf::from(""); + BedrockFilePath::from_path(&blank, &blank) + } else { + // Cannot ascend above the virtual root. + None + } + }, + } + } + } + } + + /// Returns true if the file would be hidden by the default file browser. + pub fn is_hidden(&self) -> bool { + #[cfg(target_family = "unix")] { + if let Some(stem) = self.relative.file_stem() { + if let Some(string) = stem.to_str() { + return string.starts_with('.'); + } + } + } + #[cfg(target_family = "windows")] { + use std::os::windows::fs::MetadataExt; + // See https://learn.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + // const FILE_ATTRIBUTE_HIDDEN: u32 = 0x00000002; + // const FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: u32 = 0x00002000; + if let Ok(metadata) = std::fs::metadata(self.as_path()) { + return metadata.file_attributes() & 0x2002 != 0; + } + } + return false; + } +} + + +/// 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")] { + use std::os::unix::ffi::OsStringExt; + let vec = Vec::from(slice); + return Some(OsString::from_vec(vec).into()) + } + #[cfg(target_family = "windows")] { + use std::os::windows::ffi::OsStringExt; + let mut string = String::from_utf8_lossy(slice).to_string(); + // Convert drive-current-directory paths to drive-root paths. This is + // needed because the paths C: and C:/ point to separate directories, + // but trailing forward-slashes are optional in Bedrock. + if string.ends_with(':') { + string.push('/'); + } + 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>> { + #[cfg(target_family = "unix")] + let string = path.as_os_str().to_str()?.to_string(); + #[cfg(target_family = "windows")] + let string = path.as_os_str().to_str()?.replace(r"\", r"/"); + + // Remove any trailing forward-slash and add a leading forward-slash. + let mut prefixed_string = String::from("/"); + prefixed_string.push_str(string.trim_end_matches('/')); + let slice = prefixed_string.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 relative path can be safely attached to a base without +/// breaking out of the sandbox. +fn assert_path_is_safe(relative: &Path, _base: &Path) -> Option<()> { + #[cfg(target_family = "unix")] { + // Error if path contains special components. + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + #[cfg(target_family = "windows")] { + // If the base path is empty, the relative path needs to be able to + // contain the prefix and root element. If the base path is not + // empty, the relative path must not contain these elements else + // they will override the base path when joined. + if _base.components().count() != 0 { + for component in relative.components() { + match component { + Component::Normal(_) => continue, + _ => return None, + } + } + } + } + return Some(()); +} + +fn get_entry_type(absolute: PathBuf) -> Option<EntryType> { + #[cfg(target_family = "windows")] { + // If path is empty, this is a virtual root directory. + if absolute.components().count() == 0 { + return Some(EntryType::Directory) + } + } + let metadata = std::fs::metadata(absolute).ok()?; + if metadata.is_file() { + Some(EntryType::File) + } else if metadata.is_dir() { + Some(EntryType::Directory) + } else { + None + } +} + +impl std::fmt::Debug for BedrockFilePath { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + self.as_path().fmt(f) + } +} + +// --------------------------------------------------------------------------- + +impl PartialEq for BedrockFilePath { + fn eq(&self, other: &Self) -> bool { + self.bytes == other.bytes && self.entry_type == other.entry_type + } +} + +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 { + match self.entry_type.cmp(&other.entry_type) { + Ordering::Equal => compare_ascii_slices(&self.bytes, &other.bytes), + ordering => ordering, + } + } +} + +/// 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: +/// +/// ```text +/// !"#$%&'()*+,-./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 + } +} |