use std::collections::BTreeMap;
use std::path::Path;
use crate::Off;
use super::{
DEFINITIONS_SUFFIX, FILES, MAX_FILE, MAX_ICON_FILE, MAX_ICONS, MAX_SCRIPT_DEPTH, MAX_SCRIPTS,
SCRIPT_EXTENSION, SCRIPT_MAIN,
};
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{file}:{line}:{column}: {message}")]
pub struct PackError {
pub file: String,
pub line: usize,
pub column: usize,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{}", self.lines())]
pub struct ParsePackError(pub(super) Vec<PackError>);
impl ParsePackError {
#[must_use]
pub fn list(&self) -> &[PackError] {
&self.0
}
fn lines(&self) -> String {
self.0
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
}
}
impl From<ParsePackError> for Off {
fn from(errors: ParsePackError) -> Self {
Self::new(format!("data pack: {errors}"))
}
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ReadPackError {
#[error("{file}: {source}")]
Io {
file: String,
source: std::io::Error,
},
#[error("{file}: larger than {limit} bytes")]
TooLarge {
file: String,
limit: u64,
},
#[error("more than {MAX_ICONS} icons")]
TooManyIcons,
#[error("more than {MAX_SCRIPTS} scripts")]
TooManyScripts,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PackFiles {
pub(super) files: BTreeMap<String, Vec<u8>>,
}
impl PackFiles {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, name: &str, bytes: impl Into<Vec<u8>>) -> Self {
self.files.insert(name.to_owned(), bytes.into());
self
}
pub fn read_dir(dir: &Path) -> Result<Self, ReadPackError> {
let mut files = Self::new();
let read =
|path: &Path, name: &str, limit: u64, files: &mut Self| -> Result<(), ReadPackError> {
let io = |source| ReadPackError::Io {
file: name.to_owned(),
source,
};
let meta = std::fs::metadata(path).map_err(io)?;
if meta.len() > limit {
return Err(ReadPackError::TooLarge {
file: name.to_owned(),
limit,
});
}
let bytes = std::fs::read(path).map_err(io)?;
files.files.insert(name.to_owned(), bytes);
Ok(())
};
for name in FILES {
let path = dir.join(name);
if path.is_file() {
read(&path, name, MAX_FILE, &mut files)?;
}
}
if let Ok(entries) = std::fs::read_dir(dir.join("icons")) {
let mut icons = 0usize;
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let png = path
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("png"));
if let (true, Some(file)) = (png, path.file_name().and_then(|f| f.to_str())) {
icons += 1;
if icons > MAX_ICONS {
return Err(ReadPackError::TooManyIcons);
}
read(&path, &format!("icons/{file}"), MAX_ICON_FILE, &mut files)?;
}
}
}
for (path, name) in scripts_in(dir)? {
read(&path, &name, MAX_FILE, &mut files)?;
}
Ok(files)
}
pub fn scripts(&self) -> impl Iterator<Item = (&str, &[u8])> {
self.files
.iter()
.filter(|(name, _)| is_script(name))
.map(|(name, bytes)| (name.as_str(), bytes.as_slice()))
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&[u8]> {
self.files.get(name).map(Vec::as_slice)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.files.keys().map(String::as_str)
}
#[must_use]
pub fn hash(&self) -> u32 {
let mut hash: u32 = 0x811c_9dc5;
let mut eat = |bytes: &[u8]| {
for &b in bytes {
hash ^= u32::from(b);
hash = hash.wrapping_mul(0x0100_0193);
}
};
for (name, bytes) in &self.files {
eat(name.as_bytes());
eat(&[0]);
eat(&u64::try_from(bytes.len()).unwrap_or(u64::MAX).to_le_bytes());
eat(bytes);
}
hash
}
pub(super) fn text(&self, name: &str) -> Result<Option<&str>, PackError> {
self.get(name)
.map(|bytes| {
core::str::from_utf8(bytes)
.map_err(|e| PackError::at(name, "", 0, &format!("not UTF-8: {e}")))
})
.transpose()
}
}
impl PackError {
pub(super) fn at(file: &str, text: &str, offset: usize, message: &str) -> Self {
let before = text.get(..offset.min(text.len())).unwrap_or("");
let line = before.matches('\n').count() + 1;
let column = before
.rsplit_once('\n')
.map_or(before, |(_, last)| last)
.chars()
.count()
+ 1;
Self {
file: file.to_owned(),
line,
column,
message: message.to_owned(),
}
}
pub(super) fn of(file: &str, text: &str, error: &toml::de::Error) -> Self {
let offset = error.span().map_or(0, |span| span.start);
Self::at(file, text, offset, error.message().trim_end())
}
}
pub fn script_files(dir: &Path) -> Result<Vec<std::path::PathBuf>, ReadPackError> {
Ok(scripts_in(dir)?.into_iter().map(|(path, _)| path).collect())
}
fn scripts_in(dir: &Path) -> Result<Vec<(std::path::PathBuf, String)>, ReadPackError> {
let mut scripts = Vec::new();
if dir.join(SCRIPT_MAIN).is_file() {
find_scripts(dir, "", 0, &mut scripts)?;
}
Ok(scripts)
}
fn is_script(name: &str) -> bool {
name.rsplit_once('.')
.is_some_and(|(_, ext)| ext == SCRIPT_EXTENSION)
&& !name.ends_with(DEFINITIONS_SUFFIX)
}
fn find_scripts(
dir: &Path,
prefix: &str,
depth: usize,
found: &mut Vec<(std::path::PathBuf, String)>,
) -> Result<(), ReadPackError> {
let io = |source| ReadPackError::Io {
file: if prefix.is_empty() {
".".to_owned()
} else {
prefix.to_owned()
},
source,
};
for entry in std::fs::read_dir(dir).map_err(io)? {
let entry = entry.map_err(io)?;
let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if file.starts_with('.') {
continue;
}
let name = format!("{prefix}{file}");
let path = entry.path();
let kind = entry.file_type().map_err(io)?;
if kind.is_dir() {
if depth < MAX_SCRIPT_DEPTH {
find_scripts(&path, &format!("{name}/"), depth + 1, found)?;
}
} else if is_script(&name) && path.is_file() {
found.push((path, name));
if found.len() > MAX_SCRIPTS {
return Err(ReadPackError::TooManyScripts);
}
}
}
Ok(())
}