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, } 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)?; 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 { 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 { 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::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 { // 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 = 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> { 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 { 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 } }