use super::*; use std::cmp::Ordering; use std::ffi::OsString; #[derive(Clone)] pub struct BedrockFilePath { base: PathBuf, relative: PathBuf, bytes: Vec, entry_type: Option, } impl BedrockFilePath { pub fn from_buffer(buffer: [u8; 256], base: &Path) -> Option { 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 { 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 { self.entry_type } /// Get a path which represents the parent of this path. pub fn parent(&self) -> Option { #[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 { // 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 = 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> { #[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 { #[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 { 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: // !"#$%&'()*+,-./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 } }