use super::*;
use std::cmp::Ordering;
use std::ffi::OsString;
#[derive(Clone)]
pub struct BedrockFilePath {
base: PathBuf,
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:
// !"#$%&'()*+,-./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
}
}