diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/baas.rs | 224 | ||||
-rw-r--r-- | src/contentdb.rs | 127 | ||||
-rw-r--r-- | src/download.rs | 161 | ||||
-rw-r--r-- | src/error.rs | 64 | ||||
-rw-r--r-- | src/game.rs | 74 | ||||
-rw-r--r-- | src/kvstore.rs | 49 | ||||
-rw-r--r-- | src/lib.rs | 104 | ||||
-rw-r--r-- | src/minemod.rs | 242 | ||||
-rw-r--r-- | src/util.rs | 62 | ||||
-rw-r--r-- | src/world.rs | 97 |
10 files changed, 0 insertions, 1204 deletions
diff --git a/src/baas.rs b/src/baas.rs deleted file mode 100644 index 938b4c4..0000000 --- a/src/baas.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! This module contains functions to query & manipulate the global Minetest installation. -use std::{collections::HashMap, path::PathBuf}; - -use dirs; -use log::debug; - -use super::{ - error::Result, - game::Game, - minemod::{self, MineMod}, - scan, - world::World, -}; - -/// Returns a list of folders in which worlds are expected. -/// -/// Note that not all of these folders need to exist. -/// -/// This returns the following locations: -/// -/// * `$HOME/.minetest/worlds` -/// * `/var/games/minetest-server/.minetest/worlds` -pub fn world_dirs() -> Result<Vec<PathBuf>> { - let mut paths = vec!["/var/games/minetest-server/.minetest/worlds".into()]; - if let Some(home) = dirs::home_dir() { - paths.push(home.join(".minetest").join("worlds")) - } - Ok(paths) -} - -/// Returns a list of folders in which games are expected. -/// -/// Note that not all of these folders need to exist. -/// -/// This returns the following locations: -/// -/// * `$HOME/.minetest/games` -/// * `/var/games/minetest-server/.minetest/games` -/// * `/usr/share/minetest/games` -/// * `/usr/share/games/minetest/games` -pub fn game_dirs() -> Result<Vec<PathBuf>> { - let mut paths = vec![ - "/var/games/minetest-server/.minetest/games".into(), - "/usr/share/minetest/games".into(), - "/usr/share/games/minetest/games".into(), - ]; - if let Some(home) = dirs::home_dir() { - paths.push(home.join(".minetest").join("games")) - } - Ok(paths) -} - -/// Returns a list of folders in which mods are expected. -/// -/// Note that not all of these folders need to exist. -/// -/// This returns the following locations: -/// -/// * `$HOME/.minetest/mods` -/// * `/var/games/minetest-server/.minetest/mods` -/// * `/usr/share/games/minetest/mods` -/// * `/usr/share/minetest/mods` -pub fn mod_dirs() -> Result<Vec<PathBuf>> { - let mut paths = vec![ - "/var/games/minetest-server/.minetest/mods".into(), - "/usr/share/games/minetest/mods".into(), - "/usr/share/minetest/mods".into(), - ]; - if let Some(home) = dirs::home_dir() { - paths.push(home.join(".minetest").join("mods")) - } - Ok(paths) -} - -/// The [`Baas`] provides a way to list all worlds, games and mods on the system and allows access -/// via the [`World`], [`Game`] and [`MineMod`] wrappers. -#[derive(Debug, Default, Clone)] -pub struct Baas { - world_dirs: Vec<PathBuf>, - game_dirs: Vec<PathBuf>, - mod_dirs: Vec<PathBuf>, -} - -impl Baas { - /// Create a [`Baas`] with the standard dirs. - pub fn with_standard_dirs() -> Result<Baas> { - Ok(Baas::default() - .with_world_dirs(world_dirs()?) - .with_game_dirs(game_dirs()?) - .with_mod_dirs(mod_dirs()?)) - } - - /// Replace the world dirs with the given list of world dirs. - pub fn with_world_dirs(self, world_dirs: Vec<PathBuf>) -> Baas { - Baas { world_dirs, ..self } - } - - /// The list of directories which are searched for worlds. - #[inline] - pub fn world_dirs(&self) -> &[PathBuf] { - self.world_dirs.as_slice() - } - - /// Replace the game dirs with the given list of game dirs. - pub fn with_game_dirs(self, game_dirs: Vec<PathBuf>) -> Baas { - Baas { game_dirs, ..self } - } - - /// The list of directories which are searched for games. - #[inline] - pub fn game_dirs(&self) -> &[PathBuf] { - self.game_dirs.as_slice() - } - - /// Replace the mod dirs with the given list of mod dirs. - pub fn with_mod_dirs(self, mod_dirs: Vec<PathBuf>) -> Baas { - Baas { mod_dirs, ..self } - } - - /// The list of directories which are searched for mods. - #[inline] - pub fn mod_dirs(&self) -> &[PathBuf] { - self.mod_dirs.as_slice() - } - - /// Returns a vector of all words that were found in the world dirs. - pub fn worlds(&self) -> Result<Vec<World>> { - let mut worlds = vec![]; - for dir in self.world_dirs() { - match scan(&dir, |p| World::open(p)) { - Ok(w) => worlds.extend(w), - Err(e) => debug!("Cannot scan {:?}: {}", dir, e), - } - } - Ok(worlds) - } - - /// Returns a vector of all games that were found in the game dirs. - pub fn games(&self) -> Result<Vec<Game>> { - let mut games = vec![]; - for dir in self.game_dirs() { - match scan(&dir, |p| Game::open(p)) { - Ok(g) => games.extend(g), - Err(e) => debug!("Cannot scan {:?}: {}", dir, e), - } - } - Ok(games) - } - - /// Returns a vector of all mods that were found in the mod dirs. - /// - /// Note that modpacks are flattened into mods. - pub fn mods(&self) -> Result<Vec<MineMod>> { - let mut mods = vec![]; - for dir in self.mod_dirs() { - match scan(&dir, |p| minemod::open_mod_or_pack(p)) { - Ok(m) => { - for container in m { - mods.extend(container.mods()?); - } - } - Err(e) => debug!("Cannot scan {:?}: {}", dir, e), - } - } - Ok(mods) - } - - /// Return a snapshot of the current state. - /// - /// A snapshot "freezes" the lists of worlds, mods and games in time. It is useful to avoid - /// unnecessary I/O when it is known that the state should not have changed. It also allows - /// fast searching of items by their name. - pub fn snapshot(&self) -> Result<Snapshot> { - let worlds = self.worlds()?; - let games = self.games()?; - let mods = self.mods()?; - - Ok(Snapshot { - worlds: worlds - .into_iter() - .map(|w| Ok((w.world_name()?, w))) - .collect::<Result<_>>()?, - games: games.into_iter().map(|g| (g.technical_name(), g)).collect(), - mods: mods - .into_iter() - .map(|m| Ok((m.mod_id()?, m))) - .collect::<Result<_>>()?, - }) - } -} - -/// Snapshot of a [`Baas`] scan. -/// -/// A snapshot is created through the [`Baas::snapshot`] method and gives a frozen view on the -/// installed objects. -#[derive(Debug, Clone)] -pub struct Snapshot { - worlds: HashMap<String, World>, - games: HashMap<String, Game>, - mods: HashMap<String, MineMod>, -} - -impl Snapshot { - /// Return all worlds that were found. - /// - /// The map maps the world's name to the [`World`] object. - pub fn worlds(&self) -> &HashMap<String, World> { - &self.worlds - } - - /// Return all games that were found. - /// - /// The map maps the game ID to the [`Game`] object. - pub fn games(&self) -> &HashMap<String, Game> { - &self.games - } - - /// Return the available mods that were found. - /// - /// The map maps the mod name to the [`MineMod`] object. - pub fn mods(&self) -> &HashMap<String, MineMod> { - &self.mods - } -} diff --git a/src/contentdb.rs b/src/contentdb.rs deleted file mode 100644 index d9c4688..0000000 --- a/src/contentdb.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Module to interact with the Minetest Content DB website. - -use once_cell::sync::Lazy; -use scraper::{Html, Selector}; -use serde::{Deserialize, Serialize}; -use url::Url; - -use super::error::{Error, Result}; - -/// The identification of content on Content DB. Consists of the username and the package name. -pub type ContentId = (String, String); - -/// The URL of the default Content DB website to use. -pub static DEFAULT_INSTANCE: Lazy<Url> = - Lazy::new(|| Url::parse("https://content.minetest.net/").expect("Invalid default URL")); - -/// The metapackage selector to scrape the packages. -static PROVIDES_SELECTOR: Lazy<Selector> = - Lazy::new(|| Selector::parse("ul.d-flex").expect("Invalid selector")); - -static A_SELECTOR: Lazy<Selector> = Lazy::new(|| Selector::parse("a").expect("Invalid selector")); - -/// (Partial) metadata of a content item, as returned by the Content DB API. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] -pub struct ContentMeta { - /// Username of the author. - pub author: String, - /// Name of the package. - pub name: String, - /// A list of mods that are provided by this package. - pub provides: Vec<String>, - /// The short description of the package. - pub short_description: String, - /// The (human-readable) title of this package. - pub title: String, - /// The type of the package ("mod", "game", "txp") - #[serde(rename = "type")] - pub typ: String, - /// The download URL of the package. - pub url: Url, -} - -/// The main access point for Content DB queries. -#[derive(Debug, Clone)] -pub struct ContentDb { - base_url: Url, -} - -impl Default for ContentDb { - fn default() -> Self { - Self::new() - } -} - -impl ContentDb { - /// Create a new Content DB accessor pointing to the default instance. - pub fn new() -> ContentDb { - ContentDb { - base_url: DEFAULT_INSTANCE.clone(), - } - } - - /// Find suitable candidates that provide the given modname. - pub fn resolve(&self, modname: &str) -> Result<Vec<ContentMeta>> { - let path = format!("metapackages/{}", modname); - let endpoint = self - .base_url - .join(&path) - .map_err(|_| Error::InvalidModId(modname.into()))?; - - let body = ureq::request_url("GET", &endpoint).call()?.into_string()?; - - let dom = Html::parse_document(&body); - let provides = dom - .select(&PROVIDES_SELECTOR) - .next() - .ok_or(Error::InvalidScrape)?; - - let candidates: Vec<ContentId> = provides - .select(&A_SELECTOR) - .filter_map(|a| a.value().attr("href")) - .filter_map(extract_content_id) - .collect(); - - let mut good_ones = Vec::new(); - - for (user, package) in candidates { - let path = format!("api/packages/{}/{}/", user, package); - let endpoint = self - .base_url - .join(&path) - .expect("The parsed path was wrong"); - let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?; - - // While resolving, we only care about actual mods that we can install. If a game - // provides a certain metapackage, it is pretty much useless for us (and often just - // there because a mod in that game provides the metapackage). - if response.typ == "mod" { - good_ones.push(response) - } - } - - Ok(good_ones) - } - - /// Retrieve the download url for a given package. - pub fn download_url(&self, user: &str, package: &str) -> Result<Url> { - let path = format!("api/packages/{}/{}/", user, package); - let endpoint = self - .base_url - .join(&path) - .expect("The parsed path was wrong"); - let response: ContentMeta = ureq::request_url("GET", &endpoint).call()?.into_json()?; - Ok(response.url) - } -} - -fn extract_content_id(path: &str) -> Option<ContentId> { - regex!("/packages/([^/]+)/([^/]+)/$") - .captures(path) - .map(|c| { - ( - c.get(1).unwrap().as_str().into(), - c.get(2).unwrap().as_str().into(), - ) - }) -} diff --git a/src/download.rs b/src/download.rs deleted file mode 100644 index b9507b7..0000000 --- a/src/download.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! Module to download mods from the internet. -//! -//! This module allows to download mods from various sources in the internet. Source specification -//! is done through the [`Source`] enum: -//! -//! * [`Source::Http`]: Download straight from an URL. It is expected that the URL points to a zip -//! archive which contains the mod, either directly or in a subfolder. -//! * [`Source::ContentDb`]: Refers to a package on the ContentDB. The [`Downloader`] will consult -//! the API to get the right download URL. -//! * [`Source::ModId`]: Refers to a simple mod name. Note that this specification can be -//! ambiguous, in which case the [`Downloader`] will return an error. -//! -//! The actual download work is done by a [`Downloader`]. Each [`Downloader`] has its own temporary -//! directory, in which any mods are downloaded and extracted. If you drop the [`Downloader`], -//! those mods will be deleted and the objects pointing to the now-gone directories are no longer -//! useful. - -use std::{ - fs, - io::{Cursor, Read}, - str::FromStr, -}; - -use tempdir::TempDir; -use url::Url; -use uuid::Uuid; -use zip::ZipArchive; - -use super::{ - contentdb::{ContentDb, ContentId}, - error::{Error, Result}, - minemod::{self, ModContainer, ModId}, -}; - -/// A source determines where a mod should be loaded from. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Source { - /// Download a mod archive through HTTP. - Http(Url), - /// Download a mod from the Content DB, using the given user- and package name. - ContentDb(ContentId), - /// Search the Content DB for a given mod ID. - /// - /// The download may fail if there are multiple mods providing the same ID. - ModId(ModId), -} - -impl FromStr for Source { - type Err = Error; - - fn from_str(s: &str) -> Result<Self, Self::Err> { - if s.starts_with("http://") || s.starts_with("https://") { - let url = Url::parse(s)?; - return Ok(Source::Http(url)); - } - let groups = regex!("^([^/]+)/([^/]+)$").captures(s); - if let Some(groups) = groups { - return Ok(Source::ContentDb(( - groups.get(1).unwrap().as_str().into(), - groups.get(2).unwrap().as_str().into(), - ))); - } - - if !s.contains(' ') { - return Ok(Source::ModId(s.into())); - } - - Err(Error::InvalidSourceSpec(s.into())) - } -} - -/// A downloader is responsible for downloading mods from various sources. -/// -/// Note that the objects that the [`Downloader`] creates will not work after the downloader has -/// been destroyed, as the temporary files will be lost. -#[derive(Debug)] -pub struct Downloader { - temp_dir: TempDir, - content_db: ContentDb, -} - -impl Downloader { - /// Create a new [`Downloader`], refering to the default ContentDB. - pub fn new() -> Result<Downloader> { - Downloader::with_content_db(Default::default()) - } - - /// Create a new [`Downloader`] that points to a specific ContentDB instance. - pub fn with_content_db(content_db: ContentDb) -> Result<Downloader> { - let temp_dir = TempDir::new(env!("CARGO_PKG_NAME"))?; - Ok(Downloader { - temp_dir, - content_db, - }) - } - - /// Download a mod from the given source. - /// - /// This function may download either a mod ([`minemod::MineMod`]) or a modpack - /// ([`minemod::Modpack`]), therefore it returns a trait object that can be queried for the - /// required information. - /// - /// Note that the object will be useless when the [`Downloader`] is dropped, as the temporary - /// directory containing the downloaded data will be lost. Use [`ModContainer::install_to`] to - /// copy the mod content to a different directory. - pub fn download(&self, source: &Source) -> Result<Box<dyn ModContainer>> { - match *source { - Source::Http(ref url) => self.download_http(url), - Source::ContentDb((ref user, ref package)) => { - let url = self.content_db.download_url(user, package)?; - self.download_http(&url) - } - Source::ModId(ref id) => { - let candidates = self.content_db.resolve(id)?; - if candidates.len() != 1 { - return Err(Error::AmbiguousModId(id.into())); - } - self.download_http(&candidates[0].url) - } - } - } - - /// Downloads a mod given a HTTP link. - /// - /// The [`Downloader`] expects to receive a zipfile containing the mod directory on this link. - /// - /// Refer to the module level documentation and [`Downloader::download`] for more information. - pub fn download_http(&self, url: &Url) -> Result<Box<dyn ModContainer>> { - let mut reader = ureq::request_url("GET", url).call()?.into_reader(); - let mut data = Vec::new(); - reader.read_to_end(&mut data)?; - let data = Cursor::new(data); - let mut archive = ZipArchive::new(data)?; - - let dir = self - .temp_dir - .path() - .join(&Uuid::new_v4().to_hyphenated().to_string()); - fs::create_dir(&dir)?; - - archive.extract(&dir)?; - - // Some archives contain the mod files directly, so try to open it: - if let Ok(pack) = minemod::open_mod_or_pack(&dir) { - return Ok(pack); - } - - // If the archive does not contain the mod directly, we instead try the subdirectories that - // we've extracted. - for entry in fs::read_dir(&dir)? { - let entry = entry?; - let metadata = fs::metadata(&entry.path())?; - if metadata.is_dir() { - if let Ok(pack) = minemod::open_mod_or_pack(&entry.path()) { - return Ok(pack); - } - } - } - Err(Error::InvalidModDir(dir)) - } -} diff --git a/src/error.rs b/src/error.rs deleted file mode 100644 index 5dbd6b6..0000000 --- a/src/error.rs +++ /dev/null @@ -1,64 +0,0 @@ -//! Error definitions for ModderBaas. -//! -//! The type alias [`Result`] can be used, which defaults the error type to [`enum@Error`]. Any function -//! that introduces errors should return a [`Result`] — unless it is clear that a more narrow error -//! will suffice, such as [`crate::util::copy_recursive`]. -use std::path::PathBuf; - -use thiserror::Error; - -/// The main error type. -#[derive(Error, Debug)] -pub enum Error { - /// A malformed or otherwise invalid mod ID has been given. - #[error("invalid mod id '{0}'")] - InvalidModId(String), - /// The ContentDB website returned invalid data. - #[error("the website returned unexpected data")] - InvalidScrape, - /// The directory does not contain a valid Minetest mod. - #[error("'{0}' is not a valid mod directory")] - InvalidModDir(PathBuf), - /// The directory does not contain a valid Minetest game. - #[error("'{0}' is not a valid game directory")] - InvalidGameDir(PathBuf), - /// The directory does not contain a valid Minetest world. - #[error("'{0}' is not a valid world directory")] - InvalidWorldDir(PathBuf), - /// The directory does not contain a valid Minetest modpack. - #[error("'{0}' is not a valid modpack directory")] - InvalidModpackDir(PathBuf), - /// The given source string can not be parsed into a [`crate::download::Source`]. - #[error("'{0}' does not represent a valid mod source")] - InvalidSourceSpec(String), - - /// An empty ZIP archive was downloaded. - #[error("the downloaded file was empty")] - EmptyArchive, - /// The given world does not have a game ID set. - #[error("the world has no game ID set")] - NoGameSet, - /// ContentDB returned more than one fitting mod for the query. - #[error("the mod ID '{0}' does not point to a single mod")] - AmbiguousModId(String), - - /// Wrapper for HTTP errors. - #[error("underlying HTTP error")] - UreqError(#[from] ureq::Error), - /// Wrapper for generic I/O errors. - #[error("underlying I/O error")] - IoError(#[from] std::io::Error), - /// Wrapper for ZIP errors. - #[error("ZIP error")] - ZipError(#[from] zip::result::ZipError), - /// Wrapper for URL parsing errors. - #[error("invalid URL")] - UrlError(#[from] url::ParseError), - /// Wrapper for Unix errors. - #[cfg(unix)] - #[error("underlying Unix error")] - NixError(#[from] nix::errno::Errno), -} - -/// A [`Result`] with the error type defaulted to [`enum@Error`]. -pub type Result<T, E = Error> = std::result::Result<T, E>; diff --git a/src/game.rs b/src/game.rs deleted file mode 100644 index 0d2405d..0000000 --- a/src/game.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Module to interact with installed games. -//! -//! The main type of this module is a [`Game`], which can be constructed by opening installed -//! Minetest games through [`Game::open`]. -//! -//! Valid game directories have a `game.conf` configuration file which contains some metadata about -//! the game. -use std::{ - fmt, - path::{Path, PathBuf}, -}; - -use super::{ - error::{Error, Result}, - minemod::{self, MineMod}, - scan, -}; - -/// Represents an on-disk Minetest game. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Game { - path: PathBuf, -} - -impl Game { - /// Open the given directory as a Minetest game. - /// - /// Note that the path may be relative, but only to the parent directory of the actual game. - /// This is because Minetest uses the game's directory name to identify the game - /// ([`Game::technical_name`]), so we need this information. - pub fn open<P: AsRef<Path>>(path: P) -> Result<Game> { - Game::open_path(path.as_ref()) - } - - fn open_path(path: &Path) -> Result<Game> { - if path.file_name().is_none() { - return Err(Error::InvalidGameDir(path.into())); - } - let conf = path.join("game.conf"); - if !conf.is_file() { - return Err(Error::InvalidGameDir(path.into())); - } - - Ok(Game { path: path.into() }) - } - - /// Returns the technical name of this game. - /// - /// This is the name that is used by minetest to identify the game. - pub fn technical_name(&self) -> String { - self.path - .file_name() - .expect("Somebody constructed an invalid `Game`") - .to_str() - .expect("Non-UTF8 directory encountered") - .into() - } - - /// Returns all mods that this game provides. - pub fn mods(&self) -> Result<Vec<MineMod>> { - let path = self.path.join("mods"); - let mut mods = vec![]; - for container in scan(&path, |p| minemod::open_mod_or_pack(p))? { - mods.extend(container.mods()?); - } - Ok(mods) - } -} - -impl fmt::Display for Game { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.technical_name()) - } -} diff --git a/src/kvstore.rs b/src/kvstore.rs deleted file mode 100644 index 006bb94..0000000 --- a/src/kvstore.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! Support module for writing `key=value` stores. -//! -//! These files are used by minetest mods (`mod.conf`), games (`game.conf`) and worlds -//! (`world.mt`). -//! -//! Key-Value-Stores (KVStores) are represented by a [`std::collections::HashMap`] on the Rust -//! side. -use std::{collections::HashMap, fs, io::Write, path::Path}; - -use super::error::Result; - -/// Read the given file as a KVStore. -pub fn read<P: AsRef<Path>>(path: P) -> Result<HashMap<String, String>> { - read_inner(path.as_ref()) -} - -fn read_inner(path: &Path) -> Result<HashMap<String, String>> { - let content = fs::read_to_string(path)?; - - Ok(content - .lines() - .map(|line| line.splitn(2, '=').map(str::trim).collect::<Vec<_>>()) - .map(|v| (v[0].into(), v[1].into())) - .collect()) -} - -/// Write the given KVStore back to the file. -/// -/// The order of the keys is guaranteed to be the following: -/// -/// 1. All options that *don't* start with `"load_mod_"` are saved in alphabetical order. -/// 2. All remaining options are saved in alphabetical order. -/// -/// Note that this function will **override** existing files! -pub fn write<P: AsRef<Path>>(data: &HashMap<String, String>, path: P) -> Result<()> { - write_inner(data, path.as_ref()) -} - -fn write_inner(data: &HashMap<String, String>, path: &Path) -> Result<()> { - let mut items = data.iter().collect::<Vec<_>>(); - items.sort_by_key(|i| (if i.0.starts_with("load_mod_") { 1 } else { 0 }, i.0)); - - let mut output = fs::File::create(path)?; - for (key, value) in items { - writeln!(output, "{} = {}", key, value)?; - } - - Ok(()) -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index f9b1c8b..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,104 +0,0 @@ -//! ModderBaas is a library that allows you to manage Minetest mods. -//! -//! The main point of the library is to be used by the `modderbaas` CLI application, however it can -//! also be used in other Rust programs that want a (limited) API to interact with Minetest -//! content. -//! -//! # Representation -//! -//! Most objects ([`Game`], [`World`], [`MineMod`], [`Modpack`]) are represented by the path to the -//! on-disk directory that contains the corresponding game/world/mod/modpack. The initializers take -//! care that the directory contains a valid object. -//! -//! Many of the query operations do not do any caching, so avoid calling them in tight loops. If -//! you want to do a lot of name queries (e.g. to find installed mods), consider using a -//! [`Snapshot`]. -//! -//! # Racing -//! -//! ModderBaas expects that during the time of its access, no other application meddles with the -//! directories. It will *not* lead to crashes, but you will get a lot more error return values. -//! -//! # Mutability -//! -//! Some objects support mutating their state. This is usually done by directly writing the data -//! into the corresponding file. Therefore, those methods (even though they mutate state) only take -//! a shared reference (`&self`) — keep that in mind! -//! -//! # Crate Structure -//! -//! Most game objects are implemented in their own module and re-exported at the crate root: -//! -//! * [`game`] -//! * [`minemod`] -//! * [`world`] -//! -//! Interacting with mods from the internet/ContentDB is done through two modules: -//! -//! * [`contentdb`] -//! * [`download`] -//! -//! Some modules contain auxiliary functions: -//! -//! * [`kvstore`] -//! * [`util`] -//! -//! Error definitions are in [`error`]. -//! -//! [`baas`] contains functions and data to interact with the global Minetest installation. -use std::{fs, path::Path}; - -use log::debug; - -macro_rules! regex { - ($re:literal $(,)?) => {{ - static RE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new(); - RE.get_or_init(|| regex::Regex::new($re).unwrap()) - }}; -} - -pub mod baas; -pub mod contentdb; -pub mod download; -pub mod error; -pub mod game; -pub mod kvstore; -pub mod minemod; -pub mod util; -pub mod world; - -pub use baas::{Baas, Snapshot}; -pub use contentdb::ContentDb; -pub use download::{Downloader, Source}; -pub use error::Error; -pub use game::Game; -pub use minemod::{MineMod, Modpack}; -pub use world::World; - -use error::Result; - -/// Scan all files in the given directory. -/// -/// Files for which `scanner` returns `Ok(..)` will be collected and returned. Files for which -/// `scanner` returns `Err(..)` will be silently discarded. -/// -/// This function is useful to iterate through the items in a directory and find fitting objects: -/// -/// ```rust -/// use modderbaas::minemod::MineMod; -/// let mods = scan("/tmp", |p| MineMod::open(p))?; -/// ``` -pub fn scan<W, P: AsRef<Path>, F: for<'p> Fn(&'p Path) -> Result<W>>( - path: P, - scanner: F, -) -> Result<Vec<W>> { - debug!("Scanning through {:?}", path.as_ref()); - let mut good_ones = vec![]; - for entry in fs::read_dir(path)? { - let entry = entry?; - if let Ok(i) = scanner(&entry.path()) { - good_ones.push(i); - } - } - Ok(good_ones) -} diff --git a/src/minemod.rs b/src/minemod.rs deleted file mode 100644 index 456d1c6..0000000 --- a/src/minemod.rs +++ /dev/null @@ -1,242 +0,0 @@ -//! Module to interact with installed mods and modpacks. -//! -//! Due to technical reasons (`mod` being a Rust keyword), this module is called `minemod` and the -//! mod objects are called [`MineMod`]. -//! -//! Simple mods are represented by [`MineMod`], which can be opened by [`MineMod::open`]. Valid -//! mods are identified by their `mod.conf`, which contains metadata about the mod. -//! -//! Modpacks can be represented by [`Modpack`] and loaded through [`Modpack::open`]. A modpack is -//! just a collection of mods grouped together, and the modpack directory needs to have a -//! `modpack.conf` in its directory. -//! -//! # Mods and Packs United -//! -//! In some cases, we cannot know in advance whether we are dealing with a mod or a modpack (e.g. -//! when downloading content from ContentDB). Therefore, the trait [`ModContainer`] exists, which -//! can be used as a trait object (`Box<dyn ModContainer>`). It provides the most important methods -//! and allows downcasting through `Any`. -//! -//! If you want to work with the mods directly, you can use [`ModContainer::mods`], which returns -//! the mod itself for [`MineMod`]s, and all contained mods for [`Modpack`]s. -//! -//! If you want to open a directory as a [`ModContainer`], you can use [`open_mod_or_pack`]. - -use std::{ - any::Any, - collections::HashMap, - fmt, fs, - path::{Path, PathBuf}, -}; - -use super::{ - error::{Error, Result}, - kvstore, scan, util, -}; - -/// The type of the ID that is used to identify Minetest mods. -pub type ModId = String; - -/// A minemod is a mod that is saved somewhere on disk. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct MineMod { - path: PathBuf, -} - -impl MineMod { - /// Opens the given directory as a mod. - pub fn open<P: AsRef<Path>>(path: P) -> Result<MineMod> { - MineMod::open_path(path.as_ref()) - } - - fn open_path(path: &Path) -> Result<MineMod> { - let conf = path.join("mod.conf"); - if !conf.is_file() { - return Err(Error::InvalidModDir(path.into())); - } - - Ok(MineMod { path: path.into() }) - } - - /// Returns the path of this mod. - pub fn path(&self) -> &Path { - &self.path - } - - fn read_conf(&self) -> Result<HashMap<String, String>> { - let conf = self.path.join("mod.conf"); - kvstore::read(&conf) - } - - /// Read the mod ID. - pub fn mod_id(&self) -> Result<ModId> { - let conf = self.read_conf()?; - conf.get("name") - .map(Into::into) - .ok_or_else(|| Error::InvalidModDir(self.path.clone())) - } - - /// Returns all dependencies of this mod. - pub fn dependencies(&self) -> Result<Vec<ModId>> { - let conf = self.read_conf()?; - static EMPTY: String = String::new(); - let depstr = conf.get("depends").unwrap_or(&EMPTY); - Ok(depstr - .split(',') - .map(str::trim) - .filter(|s| !s.is_empty()) - .map(Into::into) - .collect()) - } - - /// Copies the mod to the given path. - /// - /// Note that the path should not include the mod directory, that will be appended - /// automatically. - /// - /// Returns a new [`MineMod`] object pointing to the copy. - pub fn copy_to<P: AsRef<Path>>(&self, path: P) -> Result<MineMod> { - let path = path.as_ref().join(self.mod_id()?); - fs::create_dir_all(&path)?; - util::copy_recursive(&self.path, &path)?; - MineMod::open(&path) - } -} - -impl fmt::Display for MineMod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.mod_id().map_err(|_| fmt::Error)?) - } -} - -/// Represents an on-disk modpack. -/// -/// We don't support many modpack operations besides listing the modpack contents. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Modpack { - path: PathBuf, -} - -impl Modpack { - /// Opens the given directory as a modpack. - pub fn open<P: AsRef<Path>>(path: P) -> Result<Modpack> { - Modpack::open_path(path.as_ref()) - } - - fn open_path(path: &Path) -> Result<Modpack> { - let conf = path.join("modpack.conf"); - if !conf.is_file() { - return Err(Error::InvalidModpackDir(path.into())); - } - - Ok(Modpack { path: path.into() }) - } - - /// Returns the path of this modpack. - pub fn path(&self) -> &Path { - &self.path - } - - fn conf(&self) -> Result<HashMap<String, String>> { - let conf = self.path.join("modpack.conf"); - kvstore::read(&conf) - } - - /// Returns the name of the modpack. - pub fn name(&self) -> Result<String> { - self.conf()? - .get("name") - .map(Into::into) - .ok_or_else(|| Error::InvalidModDir(self.path.clone())) - } - - /// Return all mods contained in this modpack. - pub fn mods(&self) -> Result<Vec<MineMod>> { - let mut mods = vec![]; - for container in scan(&self.path, |p| open_mod_or_pack(p))? { - mods.extend(container.mods()?); - } - Ok(mods) - } - - /// Copies the modpack to the given path. - /// - /// Note that the path should not include the modpack directory, that will be appended - /// automatically. - /// - /// Returns a new [`Modpack`] object pointing to the copy. - pub fn copy_to<P: AsRef<Path>>(&self, path: P) -> Result<Modpack> { - let path = path.as_ref().join(self.name()?); - fs::create_dir_all(&path)?; - util::copy_recursive(&self.path, &path)?; - Modpack::open(&path) - } -} - -impl fmt::Display for Modpack { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} (pack)", self.name().map_err(|_| fmt::Error)?) - } -} - -/// A thing that can contain mods. -/// -/// This is useful for code that should deal with both mods and modpacks. -pub trait ModContainer: Any + fmt::Display { - /// Returns the name of the mod container. - fn name(&self) -> Result<String>; - - /// Returns the on-disk path of this mod container. - fn path(&self) -> &Path; - - /// Return all contained mods. - fn mods(&self) -> Result<Vec<MineMod>>; - - /// Copies the content to the given directory. - fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>>; -} - -impl ModContainer for MineMod { - fn name(&self) -> Result<String> { - self.mod_id() - } - - fn path(&self) -> &Path { - self.path() - } - - fn mods(&self) -> Result<Vec<MineMod>> { - Ok(vec![self.clone()]) - } - - fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>> { - self.copy_to(path) - .map(|x| Box::new(x) as Box<dyn ModContainer>) - } -} - -impl ModContainer for Modpack { - fn name(&self) -> Result<String> { - self.name() - } - - fn path(&self) -> &Path { - self.path() - } - - fn mods(&self) -> Result<Vec<MineMod>> { - self.mods() - } - - fn install_to(&self, path: &Path) -> Result<Box<dyn ModContainer>> { - self.copy_to(path) - .map(|x| Box::new(x) as Box<dyn ModContainer>) - } -} - -/// Attempts to open the given path as either a single mod or a modpack. -pub fn open_mod_or_pack<P: AsRef<Path>>(path: P) -> Result<Box<dyn ModContainer>> { - MineMod::open(path.as_ref()) - .map(|m| Box::new(m) as Box<dyn ModContainer>) - .or_else(|_| Modpack::open(path.as_ref()).map(|p| Box::new(p) as Box<dyn ModContainer>)) -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index ea401ba..0000000 --- a/src/util.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Utility functions. -use std::{fs, io, path::Path}; - -#[cfg(unix)] -use nix::unistd::{self, Gid, Uid}; - -use super::error::Result; - -#[cfg(not(unix))] -pub mod nix { - //! Stub mod on non-unix systems. - pub mod unistd { - pub enum Uid {} - pub enum Gid {} - } -} - -/// Recursively copy the *contents* of the given directory to the given path. -pub fn copy_recursive<S: AsRef<Path>, D: AsRef<Path>>(source: S, destination: D) -> io::Result<()> { - copy_inner(source.as_ref(), destination.as_ref()) -} - -fn copy_inner(source: &Path, destination: &Path) -> io::Result<()> { - for item in fs::read_dir(source)? { - let item = item?; - let metadata = item.metadata()?; - let item_destination = destination.join(item.file_name()); - if metadata.is_file() { - fs::copy(&item.path(), &item_destination)?; - } else if metadata.is_dir() { - fs::create_dir(&item_destination)?; - copy_inner(&item.path(), &item_destination)?; - } - } - Ok(()) -} - -/// Recursively change the owner of the given path to the given ones. -/// -/// Note that this function only works on Unix systems. **It will panic on other systems!** -pub fn chown_recursive<P: AsRef<Path>>(path: P, uid: Option<Uid>, gid: Option<Gid>) -> Result<()> { - chown_inner(path.as_ref(), uid, gid) -} - -#[cfg(unix)] -fn chown_inner(path: &Path, uid: Option<Uid>, gid: Option<Gid>) -> Result<()> { - unistd::chown(path, uid, gid)?; - - let metadata = fs::metadata(path)?; - if metadata.is_dir() { - for item in fs::read_dir(path)? { - let item = item?; - chown_inner(&item.path(), uid, gid)?; - } - } - Ok(()) -} - -#[cfg(not(unix))] -fn chown_inner(_: &Path, _: Option<Uid>, _: Option<Gid>) -> Result<()> { - panic!("chown() is not available on non-Unix systems!"); -} diff --git a/src/world.rs b/src/world.rs deleted file mode 100644 index 5dce6d0..0000000 --- a/src/world.rs +++ /dev/null @@ -1,97 +0,0 @@ -//! Module to interact with Minetest worlds (savegames). -//! -//! The main object is [`World`], which represents a Minetest world on-disk. -use std::{ - collections::HashMap, - fmt, - path::{Path, PathBuf}, -}; - -use super::{ - error::{Error, Result}, - kvstore, - minemod::ModId, -}; - -/// Represents an on-disk Minetest world. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct World { - path: PathBuf, -} - -impl World { - /// Open the given directory as a [`World`]. - pub fn open<P: AsRef<Path>>(path: P) -> Result<World> { - World::open_path(path.as_ref()) - } - - fn open_path(path: &Path) -> Result<World> { - let conf = path.join("world.mt"); - if !conf.is_file() { - return Err(Error::InvalidWorldDir(path.into())); - } - - Ok(World { path: path.into() }) - } - - fn conf(&self) -> Result<HashMap<String, String>> { - let conf = self.path.join("world.mt"); - kvstore::read(&conf) - } - - /// Returns the name of the world. - pub fn world_name(&self) -> Result<String> { - let fallback = self - .path - .file_name() - .map(|s| s.to_str().expect("Non-UTF8 directory encountered")); - - let conf = self.conf()?; - conf.get("world_name") - .map(String::as_str) - .or(fallback) - .ok_or_else(|| Error::InvalidWorldDir(self.path.clone())) - .map(Into::into) - } - - /// Extract the game that this world uses. - pub fn game_id(&self) -> Result<String> { - let conf = self.conf()?; - conf.get("gameid").ok_or(Error::NoGameSet).map(Into::into) - } - - /// Returns all mods that are loaded in this world. - /// - /// This returns mods that are explicitely loaded in the config, but not mods that are loaded - /// through the game. - pub fn mods(&self) -> Result<Vec<ModId>> { - let conf = self.conf()?; - const PREFIX_LEN: usize = "load_mod_".len(); - Ok(conf - .iter() - .filter(|(k, _)| k.starts_with("load_mod_")) - .filter(|(_, v)| *v == "true") - .map(|i| i.0[PREFIX_LEN..].into()) - .collect()) - } - - /// Enable the given mod. - /// - /// Note that this function does not ensure that the mod exists on-disk, nor does it do any - /// dependency checks. It simply adds the right `load_mod`-line to the world configuration - /// file. - pub fn enable_mod(&self, mod_id: &str) -> Result<()> { - let mut conf = self.conf()?; - let key = format!("load_mod_{}", mod_id); - conf.entry(key) - .and_modify(|e| *e = "true".into()) - .or_insert_with(|| "true".into()); - kvstore::write(&conf, &self.path.join("world.mt")) - } -} - -impl fmt::Display for World { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.world_name().map_err(|_| fmt::Error)?) - } -} |