summaryrefslogblamecommitdiff
path: root/src/devices/file/bedrock_file_path.rs
blob: c169a62f839106d3ef1ce3ef00b778663e032b3f (plain) (tree)











































































































































































































                                                                                  
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
    }
}