summaryrefslogtreecommitdiff
path: root/src/types
diff options
context:
space:
mode:
Diffstat (limited to 'src/types')
-rw-r--r--src/types/buffered_file.rs143
-rw-r--r--src/types/debug_symbols.rs59
-rw-r--r--src/types/directory_listing.rs120
-rw-r--r--src/types/entry_type.rs37
-rw-r--r--src/types/file_path.rs288
-rw-r--r--src/types/metadata.rs125
-rw-r--r--src/types/mod.rs19
-rw-r--r--src/types/path_buffer.rs60
-rw-r--r--src/types/read_buffer.rs34
-rw-r--r--src/types/sprite_buffer.rs85
10 files changed, 970 insertions, 0 deletions
diff --git a/src/types/buffered_file.rs b/src/types/buffered_file.rs
new file mode 100644
index 0000000..5cdf0ea
--- /dev/null
+++ b/src/types/buffered_file.rs
@@ -0,0 +1,143 @@
+use std::fs::File;
+use std::io::{BufReader, BufWriter, Read, Write};
+use std::io::{ErrorKind, Seek, SeekFrom};
+
+
+pub struct BufferedFile {
+ file: AccessMode,
+}
+
+impl BufferedFile {
+ pub fn new(file: File) -> Self {
+ Self {
+ file: AccessMode::Read(BufReader::new(file)),
+ }
+ }
+
+ pub fn close(&mut self) {
+ self.file = AccessMode::None;
+ }
+
+ pub fn read(&mut self) -> u8 {
+ let mut buffer = [0u8; 1];
+
+ let read_result = match &mut self.file {
+ AccessMode::Read(reader) => reader.read_exact(&mut buffer),
+ AccessMode::Write(writer) => {
+ let address = writer.stream_position().unwrap();
+ let file = std::mem::take(&mut self.file).unwrap();
+ let mut reader = BufReader::new(file);
+ reader.seek(SeekFrom::Start(address)).unwrap();
+ let read_result = reader.read_exact(&mut buffer);
+ self.file = AccessMode::Read(reader);
+ read_result
+ }
+ AccessMode::None => unreachable!(),
+ };
+
+ match read_result {
+ Ok(_) => buffer[0],
+ Err(error) => match error.kind() {
+ ErrorKind::UnexpectedEof => 0,
+ _ => { log::error!("BufferedFile::read: {error:?}"); 0 },
+ }
+ }
+ }
+
+ pub fn write(&mut self, byte: u8) {
+ let mut buffer = [byte; 1];
+
+ let write_result = match &mut self.file {
+ AccessMode::Write(writer) => writer.write_all(&mut buffer),
+ AccessMode::Read(reader) => {
+ let address = reader.stream_position().unwrap();
+ let file = std::mem::take(&mut self.file).unwrap();
+ let mut writer = BufWriter::new(file);
+ writer.seek(SeekFrom::Start(address)).unwrap();
+ let write_result = writer.write_all(&mut buffer);
+ self.file = AccessMode::Write(writer);
+ write_result
+ }
+ AccessMode::None => unreachable!(),
+ };
+
+ write_result.unwrap();
+ }
+
+ pub fn pointer(&mut self) -> u32 {
+ let position = match &mut self.file {
+ AccessMode::Read(reader) => reader.stream_position(),
+ AccessMode::Write(writer) => writer.stream_position(),
+ AccessMode::None => unreachable!(),
+ };
+ u32::try_from(position.unwrap()).unwrap_or(u32::MAX)
+ }
+
+ pub fn set_pointer(&mut self, pointer: u32) {
+ let position = SeekFrom::Start(pointer as u64);
+ match &mut self.file {
+ AccessMode::Read(reader) => reader.seek(position).unwrap(),
+ AccessMode::Write(writer) => writer.seek(position).unwrap(),
+ AccessMode::None => unreachable!(),
+ };
+ }
+
+ pub fn length(&mut self) -> u32 {
+ let length = match &mut self.file {
+ AccessMode::Read(reader) => reader.stream_len(),
+ AccessMode::Write(writer) => writer.stream_len(),
+ AccessMode::None => unreachable!(),
+ };
+ u32::try_from(length.unwrap()).unwrap_or(u32::MAX)
+ }
+
+ pub fn set_length(&mut self, length: u32) {
+ match &mut self.file {
+ AccessMode::Read(_) => {
+ let file = std::mem::take(&mut self.file).unwrap();
+ file.set_len(length as u64).unwrap();
+ self.file = AccessMode::Read(BufReader::new(file));
+ }
+ AccessMode::Write(_) => {
+ let file = std::mem::take(&mut self.file).unwrap();
+ file.set_len(length as u64).unwrap();
+ self.file = AccessMode::Read(BufReader::new(file));
+ }
+ AccessMode::None => unreachable!(),
+ };
+ }
+
+ pub fn flush(&mut self) {
+ if let AccessMode::Write(writer) = &mut self.file {
+ let _ = writer.flush();
+ }
+ }
+}
+
+impl Drop for BufferedFile {
+ fn drop(&mut self) {
+ self.flush()
+ }
+}
+
+enum AccessMode {
+ Read(BufReader<File>),
+ Write(BufWriter<File>),
+ None,
+}
+
+impl AccessMode {
+ pub fn unwrap(self) -> File {
+ match self {
+ Self::Read(reader) => reader.into_inner(),
+ Self::Write(writer) => writer.into_inner().unwrap(),
+ Self::None => unreachable!(),
+ }
+ }
+}
+
+impl Default for AccessMode {
+ fn default() -> Self {
+ Self::None
+ }
+}
diff --git a/src/types/debug_symbols.rs b/src/types/debug_symbols.rs
new file mode 100644
index 0000000..f4fc412
--- /dev/null
+++ b/src/types/debug_symbols.rs
@@ -0,0 +1,59 @@
+use crate::*;
+
+
+pub struct DebugSymbols {
+ symbols: Vec<DebugSymbol>
+}
+
+impl DebugSymbols {
+ /// Load debug symbols from a symbols file.
+ pub fn from_path_opt<P: AsRef<Path>>(path: Option<P>) -> Self {
+ let mut symbols = Vec::new();
+ if let Some(path) = path {
+ if let Ok(string) = std::fs::read_to_string(path) {
+ for line in string.lines() {
+ if let Some(symbol) = DebugSymbol::from_line(line) {
+ symbols.push(symbol);
+ }
+ }
+ }
+ }
+ symbols.sort_by_key(|s| s.address);
+ Self { symbols }
+ }
+
+ /// Return the symbol matching a given address.
+ pub fn for_address(&self, address: u16) -> Option<&DebugSymbol> {
+ if self.symbols.is_empty() { return None; }
+ let symbol = match self.symbols.binary_search_by_key(&address, |s| s.address) {
+ Ok(index) => self.symbols.get(index)?,
+ Err(index) => self.symbols.get(index.checked_sub(1)?)?,
+ };
+ Some(&symbol)
+ }
+}
+
+
+pub struct DebugSymbol {
+ pub address: u16,
+ pub name: String,
+ pub location: Option<String>,
+}
+
+impl DebugSymbol {
+ pub fn from_line(line: &str) -> Option<Self> {
+ if let Some((address, line)) = line.split_once(' ') {
+ let address = u16::from_str_radix(address, 16).ok()?;
+ if let Some((name, location)) = line.split_once(' ') {
+ let name = name.to_string();
+ let location = Some(location.to_string());
+ Some( DebugSymbol { address, name, location } )
+ } else {
+ let name = line.to_string();
+ Some( DebugSymbol { address, name, location: None } )
+ }
+ } else {
+ None
+ }
+ }
+}
diff --git a/src/types/directory_listing.rs b/src/types/directory_listing.rs
new file mode 100644
index 0000000..f079217
--- /dev/null
+++ b/src/types/directory_listing.rs
@@ -0,0 +1,120 @@
+use crate::*;
+
+
+pub struct DirectoryListing {
+ children: Vec<BedrockFilePath>,
+ length: u32,
+ selected: Option<u32>,
+ child_path_buffer: BedrockPathBuffer,
+}
+
+
+impl DirectoryListing {
+ pub fn from_path(path: &BedrockFilePath) -> Option<Self> {
+ macro_rules! unres {
+ ($result:expr) => { match $result { Ok(v) => v, Err(_) => continue} };
+ }
+ macro_rules! unopt {
+ ($option:expr) => { match $option { Some(v) => v, None => continue} };
+ }
+
+ #[cfg(target_family = "windows")] {
+ if path.as_path().components().count() == 0 {
+ return Some(Self::construct_virtual_root())
+ }
+ }
+
+ let mut children = Vec::new();
+ if let Ok(dir_listing) = std::fs::read_dir(path.as_path()) {
+ for (i, entry_result) in dir_listing.enumerate() {
+ // Firebreak to prevent emulator from consuming an absurd amount
+ // of memory when opening too large of a directory.
+ if i == (u16::MAX as usize) {
+ break;
+ }
+
+ let entry = unres!(entry_result);
+ let entry_path = unopt!(BedrockFilePath::from_path(&entry.path(), path.base()));
+ if entry_path.is_hidden() {
+ continue;
+ }
+
+ children.push(entry_path);
+ }
+ }
+
+ children.sort();
+ let length = u32::try_from(children.len()).ok()?;
+ let selected = None;
+ let child_path_buffer = BedrockPathBuffer::new();
+ Some( Self { children, length, selected, child_path_buffer } )
+ }
+
+ /// Generate entries for a virtual root directory.
+ #[cfg(target_family = "windows")]
+ fn construct_virtual_root() -> Self {
+ let mut children = Vec::new();
+ let base = PathBuf::from("");
+ let drive_bits = unsafe {
+ windows::Win32::Storage::FileSystem::GetLogicalDrives()
+ };
+ for i in 0..26 {
+ if drive_bits & (0x1 << i) != 0 {
+ let letter: char = (b'A' + i).into();
+ let path = PathBuf::from(format!("{letter}:/"));
+ if let Some(drive) = BedrockFilePath::from_path(&path, &base) {
+ children.push(drive);
+ }
+ }
+ }
+
+ let length = children.len() as u32;
+ let selected = None;
+ let child_path_buffer = BedrockPathBuffer::new();
+ Self { children, length, selected, child_path_buffer }
+ }
+
+ /// Attempts to return a directory child by index.
+ pub fn get(&self, index: u32) -> Option<&BedrockFilePath> {
+ self.children.get(index as usize)
+ }
+
+ pub fn length(&self) -> u32 {
+ self.length
+ }
+
+ /// Returns the index of the selected child, or zero if no child is selected.
+ pub fn selected(&self) -> u32 {
+ self.selected.unwrap_or(0)
+ }
+
+ /// Attempts to select a child by index.
+ pub fn set_selected(&mut self, index: u32) {
+ if let Some(child) = self.get(index) {
+ let buffer = child.as_buffer();
+ self.child_path_buffer.populate(buffer);
+ self.selected = Some(index);
+ } else {
+ self.child_path_buffer.clear();
+ self.selected = None;
+ }
+ }
+
+ pub fn deselect_child(&mut self) {
+ self.child_path_buffer.clear();
+ self.selected = None;
+ }
+
+ pub fn child_path_buffer(&mut self) -> &mut BedrockPathBuffer {
+ &mut self.child_path_buffer
+ }
+
+ pub fn child_type(&self) -> Option<EntryType> {
+ self.selected.and_then(|s| self.get(s).and_then(|i| i.entry_type()))
+ }
+
+ pub fn child_path(&self) -> Option<BedrockFilePath> {
+ self.selected.and_then(|s| self.get(s).and_then(|i| Some(i.clone())))
+ }
+}
+
diff --git a/src/types/entry_type.rs b/src/types/entry_type.rs
new file mode 100644
index 0000000..6a9ac2d
--- /dev/null
+++ b/src/types/entry_type.rs
@@ -0,0 +1,37 @@
+use crate::*;
+
+use std::cmp::Ordering;
+
+
+pub enum Entry {
+ File(BufferedFile),
+ Directory(DirectoryListing),
+}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum EntryType {
+ File,
+ Directory,
+}
+
+impl PartialOrd for EntryType {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ match (self, other) {
+ (EntryType::Directory, EntryType::Directory) => Some(Ordering::Equal ),
+ (EntryType::Directory, EntryType::File ) => Some(Ordering::Less ),
+ (EntryType::File, EntryType::Directory) => Some(Ordering::Greater),
+ (EntryType::File, EntryType::File ) => Some(Ordering::Equal ),
+ }
+ }
+}
+
+impl Ord for EntryType {
+ fn cmp(&self, other: &Self) -> Ordering {
+ match (self, other) {
+ (EntryType::Directory, EntryType::Directory) => Ordering::Equal ,
+ (EntryType::Directory, EntryType::File ) => Ordering::Less ,
+ (EntryType::File, EntryType::Directory) => Ordering::Greater,
+ (EntryType::File, EntryType::File ) => Ordering::Equal ,
+ }
+ }
+}
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
+ }
+}
diff --git a/src/types/metadata.rs b/src/types/metadata.rs
new file mode 100644
index 0000000..dde86fa
--- /dev/null
+++ b/src/types/metadata.rs
@@ -0,0 +1,125 @@
+use crate::*;
+
+
+const SMALL_ICON_LEN: usize = 3*3*8;
+const LARGE_ICON_LEN: usize = 8*8*8;
+
+pub fn parse_metadata(bytecode: &[u8]) -> Option<ProgramMetadata> {
+ MetadataParser::from_bytecode(bytecode).parse()
+}
+
+
+pub struct ProgramMetadata {
+ pub name: Option<String>,
+ pub version: Option<String>,
+ pub authors: Option<Vec<String>>,
+ pub description: Option<String>,
+ pub bg_colour: Option<Colour>,
+ pub fg_colour: Option<Colour>,
+ pub small_icon: Option<[u8; SMALL_ICON_LEN]>,
+ pub large_icon: Option<[u8; LARGE_ICON_LEN]>,
+}
+
+
+struct MetadataParser<'a> {
+ bytecode: &'a [u8],
+}
+
+impl<'a> MetadataParser<'a> {
+ pub fn from_bytecode(bytecode: &'a [u8]) -> Self {
+ Self { bytecode }
+ }
+
+ pub fn parse(self) -> Option<ProgramMetadata> {
+ // Verify metadata identifier.
+ let identifier = self.vec(0x00, 10);
+ if identifier != &[0x41,0x00,0x18,0x42,0x45,0x44,0x52,0x4F,0x43,0x4B] {
+ return None;
+ }
+
+ let (name, version) = if let Some(pointer) = self.pointer(0x0a) {
+ let string = self.string(pointer);
+ if let Some((name, version)) = string.split_once('/') {
+ (Some(name.trim().to_string()), Some(version.trim().to_string()))
+ } else {
+ (Some(string.trim().to_string()), None)
+ }
+ } else {
+ (None, None)
+ };
+
+ let authors = self.pointer(0x0c).map(|p| {
+ self.string(p).lines().map(|s| s.trim().to_string()).collect()
+ });
+
+ let description = self.pointer(0x0e).map(|p| self.string(p));
+ let bg_colour = self.pointer(0x10).map(|p| self.colour(p));
+ let fg_colour = self.pointer(0x12).map(|p| self.colour(p));
+
+ let small_icon = if let Some(pointer) = self.pointer(0x14) {
+ let vec = self.vec(pointer, SMALL_ICON_LEN);
+ Some(vec.try_into().unwrap())
+ } else {
+ None
+ };
+
+ let large_icon = if let Some(pointer) = self.pointer(0x16) {
+ let vec = self.vec(pointer, LARGE_ICON_LEN);
+ Some(vec.try_into().unwrap())
+ } else {
+ None
+ };
+
+ let metadata = ProgramMetadata {
+ name, version, authors, description,
+ bg_colour, fg_colour, small_icon, large_icon,
+ };
+ return Some(metadata);
+ }
+
+ fn byte(&self, address: usize) -> u8 {
+ match self.bytecode.get(address) {
+ Some(byte) => *byte,
+ None => 0,
+ }
+ }
+
+ fn double(&self, address: usize) -> u16 {
+ u16::from_be_bytes([
+ self.byte(address),
+ self.byte(address + 1),
+ ])
+ }
+
+ fn pointer(&self, address: usize) -> Option<usize> {
+ match self.double(address) {
+ 0 => None,
+ v => Some(v as usize),
+ }
+ }
+
+ fn vec(&self, address: usize, length: usize) -> Vec<u8> {
+ let mut vec = Vec::new();
+ for i in 0..length {
+ vec.push(self.byte(address + i));
+ }
+ return vec;
+ }
+
+ fn string(&self, address: usize) -> String {
+ let mut i = address;
+ while self.byte(i) != 0 {
+ i += 1;
+ }
+ let slice = &self.bytecode[address..i];
+ String::from_utf8_lossy(slice).to_string()
+ }
+
+ fn colour(&self, address: usize) -> Colour {
+ let double = self.double(address);
+ let r = (double >> 8 & 0xf) as u8 * 17;
+ let g = (double >> 4 & 0xf) as u8 * 17;
+ let b = (double & 0xf) as u8 * 17;
+ Colour::from_rgb(r, g, b)
+ }
+}
diff --git a/src/types/mod.rs b/src/types/mod.rs
new file mode 100644
index 0000000..730f053
--- /dev/null
+++ b/src/types/mod.rs
@@ -0,0 +1,19 @@
+mod buffered_file;
+mod debug_symbols;
+mod directory_listing;
+mod entry_type;
+mod file_path;
+mod metadata;
+mod path_buffer;
+mod read_buffer;
+mod sprite_buffer;
+
+pub use buffered_file::*;
+pub use debug_symbols::*;
+pub use directory_listing::*;
+pub use entry_type::*;
+pub use file_path::*;
+pub use metadata::*;
+pub use path_buffer::*;
+pub use read_buffer::*;
+pub use sprite_buffer::*;
diff --git a/src/types/path_buffer.rs b/src/types/path_buffer.rs
new file mode 100644
index 0000000..d6a0861
--- /dev/null
+++ b/src/types/path_buffer.rs
@@ -0,0 +1,60 @@
+pub struct BedrockPathBuffer {
+ buffer: [u8; 256],
+ pointer: u8,
+}
+
+impl BedrockPathBuffer {
+ pub fn new() -> Self {
+ Self { buffer: [0; 256] , pointer: 0 }
+ }
+
+ /// Clear the buffer, returning the previous buffer contents.
+ pub fn clear(&mut self) -> [u8; 256] {
+ self.pointer = 0;
+ std::mem::replace(&mut self.buffer, [0; 256])
+ }
+
+ /// Reset the pointer and hot-swap the byte buffer.
+ pub fn populate(&mut self, buffer: [u8; 256]) {
+ self.pointer = 0;
+ self.buffer = buffer;
+ }
+
+ /// Move internal pointer to the start of the path or file name.
+ ///
+ /// If value is non-zero, the pointer will be moved to the byte
+ /// directly following the final forward-slash.
+ pub fn set_pointer(&mut self, value: u8) {
+ self.pointer = 0;
+ // Set the pointer to the start of the filename if value is truthy.
+ if value != 0x00 {
+ for (i, c) in self.buffer.iter().enumerate() {
+ match c {
+ b'/' => self.pointer = (i as u8).saturating_add(1),
+ 0x00 => break,
+ _ => continue,
+ }
+ }
+ }
+ }
+
+ /// Read a single byte from the buffer.
+ pub fn read(&mut self) -> u8 {
+ let pointer = self.pointer as usize;
+ self.pointer = self.pointer.wrapping_add(1);
+ self.buffer[pointer]
+ }
+
+ /// Write a single byte to the buffer.
+ ///
+ /// If a null-byte is written, the buffer will be cleared and returned.
+ pub fn write(&mut self, byte: u8) -> Option<[u8; 256]> {
+ if byte == 0x00 {
+ Some(self.clear())
+ } else {
+ self.buffer[self.pointer as usize] = byte;
+ self.pointer = self.pointer.saturating_add(1);
+ None
+ }
+ }
+}
diff --git a/src/types/read_buffer.rs b/src/types/read_buffer.rs
new file mode 100644
index 0000000..7128048
--- /dev/null
+++ b/src/types/read_buffer.rs
@@ -0,0 +1,34 @@
+pub struct ReadBuffer {
+ pub bytes: Vec<u8>,
+ pub pointer: usize,
+}
+
+impl ReadBuffer {
+ pub fn new() -> Self {
+ Self {
+ bytes: Vec::new(),
+ pointer: 0,
+ }
+ }
+
+ pub fn from_str(text: &str) -> Self {
+ Self {
+ bytes: text.bytes().collect(),
+ pointer: 0,
+ }
+ }
+
+ pub fn set_str(&mut self, text: &str) {
+ self.bytes = text.bytes().collect();
+ self.pointer = 0;
+ }
+
+ pub fn read(&mut self) -> u8 {
+ let pointer = self.pointer;
+ self.pointer += 1;
+ match self.bytes.get(pointer) {
+ Some(byte) => *byte,
+ None => 0,
+ }
+ }
+}
diff --git a/src/types/sprite_buffer.rs b/src/types/sprite_buffer.rs
new file mode 100644
index 0000000..74c7b55
--- /dev/null
+++ b/src/types/sprite_buffer.rs
@@ -0,0 +1,85 @@
+use crate::*;
+
+
+pub struct SpriteBuffer {
+ pub mem: [u8; 16],
+ pub pointer: usize,
+ pub cached: Option<(Sprite, u8)>,
+}
+
+impl SpriteBuffer {
+ pub fn new() -> Self {
+ Self {
+ mem: [0; 16],
+ pointer: 0,
+ cached: None,
+ }
+ }
+
+ pub fn push_byte(&mut self, byte: u8) {
+ self.mem[self.pointer] = byte;
+ self.pointer = (self.pointer + 1) % 16;
+ self.cached = None;
+ }
+
+ pub fn read_1bit_sprite(&mut self, draw: u8) -> Sprite {
+ if let Some((sprite, transform)) = self.cached {
+ if transform == (draw & 0x77) {
+ return sprite;
+ }
+ }
+ macro_rules! c {
+ ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+ ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+ }
+ let mut sprite = [[0; 8]; 8];
+ let mut p = match draw & 0x02 != 0 {
+ true => self.pointer,
+ false => self.pointer + 8,
+ };
+ match draw & 0x07 {
+ 0x0 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+ 0x1 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } },
+ 0x2 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>(7-x) & 1; } } },
+ 0x3 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[y][x] = l>>( x) & 1; } } },
+ 0x4 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+ 0x5 => { for y in 0..8 { c!(l=mem[p++]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } },
+ 0x6 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>(7-x) & 1; } } },
+ 0x7 => { for y in 0..8 { c!(l=mem[--p]); for x in 0..8 { sprite[x][y] = l>>( x) & 1; } } },
+ _ => unreachable!(),
+ }
+ self.cached = Some((sprite, draw & 0x77));
+ return sprite;
+ }
+
+ pub fn read_2bit_sprite(&mut self, draw: u8) -> Sprite {
+ if let Some((sprite, transform)) = self.cached {
+ if transform == (draw & 0x77) {
+ return sprite;
+ }
+ }
+ macro_rules! c {
+ ($v:ident=mem[$p:ident++]) => { let $v = self.mem[$p % 16]; $p = $p.wrapping_add(1); };
+ ($v:ident=mem[--$p:ident]) => { $p = $p.wrapping_sub(1); let $v = self.mem[$p % 16]; };
+ }
+ let mut sprite = [[0; 8]; 8];
+ let mut p = match draw & 0x02 != 0 {
+ true => self.pointer,
+ false => self.pointer + 8,
+ };
+ let mut s = p + 8;
+ match draw & 0x07 {
+ 0x0 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x1 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x2 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x3 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[y][x] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x4 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x5 => for y in 0..8 { c!(l=mem[p++]); c!(h=mem[s++]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x6 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i=7-x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ 0x7 => for y in 0..8 { c!(l=mem[--p]); c!(h=mem[--s]); for x in 0..8 { let i= x; sprite[x][y] = (l>>i & 1) | (h>>i & 1) << 1; } },
+ _ => unreachable!(),
+ }
+ self.cached = Some((sprite, draw & 0x77));
+ return sprite;
+ }
+}