OneLauncher-Cracked/packages/oneclient_common/src/paths.rs

332 lines
8.7 KiB
Rust

use directories::{BaseDirs, ProjectDirs};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::domain::{ContentType, ProviderId};
use crate::error::{PathsError, PathsResult};
const QUALIFIER: &str = "org";
const ORGANIZATION: &str = "Polyfrost";
#[cfg(not(debug_assertions))]
const APPLICATION: &str = "OneClient";
#[cfg(debug_assertions)]
const APPLICATION: &str = "OneClient-dev";
pub const SETTINGS_FILE: &str = "settings.json";
pub const DATABASE_FILE: &str = "user_data.db";
static DEFAULT_DIR: OnceLock<PathBuf> = OnceLock::new();
static CONFIG_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
static DATA_DIR_OVERRIDE: OnceLock<PathBuf> = OnceLock::new();
pub fn set_launcher_dir(dir: PathBuf) {
let _ = CONFIG_DIR_OVERRIDE.set(dir.clone());
let _ = DATA_DIR_OVERRIDE.set(dir);
}
pub fn set_data_dir(dir: PathBuf) {
let _ = DATA_DIR_OVERRIDE.set(dir);
}
fn organization_dir() -> PathsResult<PathBuf> {
BaseDirs::new()
.map(|dirs| dirs.data_dir().join(ORGANIZATION))
.ok_or(PathsError::DataDirUnavailable)
}
#[must_use]
pub fn legacy_dir() -> Option<PathBuf> {
ProjectDirs::from(QUALIFIER, ORGANIZATION, APPLICATION)
.map(|dirs| dirs.data_local_dir().to_path_buf())
}
fn resolve_default_dir() -> PathsResult<PathBuf> {
if let Some(legacy) = legacy_dir()
&& legacy.join(SETTINGS_FILE).is_file()
{
return Ok(legacy);
}
standard_dir()
}
pub fn standard_dir() -> PathsResult<PathBuf> {
Ok(organization_dir()?.join(APPLICATION))
}
fn default_dir() -> PathsResult<&'static Path> {
if let Some(dir) = DEFAULT_DIR.get() {
return Ok(dir);
}
let _ = DEFAULT_DIR.set(resolve_default_dir()?);
DEFAULT_DIR
.get()
.map(PathBuf::as_path)
.ok_or(PathsError::DataDirUnavailable)
}
pub fn config_dir() -> PathsResult<&'static Path> {
if let Some(dir) = CONFIG_DIR_OVERRIDE.get() {
return Ok(dir);
}
default_dir()
}
pub fn data_dir() -> PathsResult<&'static Path> {
if let Some(dir) = DATA_DIR_OVERRIDE.get() {
return Ok(dir);
}
config_dir()
}
#[must_use]
pub fn picker_start_dir() -> Option<PathBuf> {
organization_dir()
.ok()
.filter(|dir| dir.is_dir())
.or_else(|| data_dir().ok().map(Path::to_path_buf))
}
pub fn database_file() -> PathsResult<PathBuf> {
Ok(data_dir()?.join(DATABASE_FILE))
}
pub fn settings_file() -> PathsResult<PathBuf> {
Ok(config_dir()?.join(SETTINGS_FILE))
}
pub fn damaged_settings_file() -> PathsResult<PathBuf> {
Ok(config_dir()?.join("settings.json.corrupt"))
}
pub fn auth_file() -> PathsResult<PathBuf> {
Ok(config_dir()?.join("auth.json"))
}
pub fn logs_dir() -> PathsResult<PathBuf> {
Ok(config_dir()?.join("logs"))
}
pub fn skins_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("skins"))
}
/// Returns all candidate skins directories across current data dir, release, dev, and legacy paths
pub fn all_skins_dirs() -> Vec<PathBuf> {
let mut dirs = Vec::new();
if let Ok(dir) = skins_dir() {
dirs.push(dir);
}
if let Some(base) = BaseDirs::new() {
let data = base.data_dir();
for candidate in [
data.join("Polyfrost").join("OneClient").join("skins"),
data.join("oneclient-dev").join("skins"),
data.join("oneclient").join("skins"),
] {
if !dirs.contains(&candidate) {
dirs.push(candidate);
}
}
}
dirs
}
pub fn skin_file_path(uuid: &str) -> PathsResult<PathBuf> {
Ok(skins_dir()?.join(format!("{uuid}.png")))
}
pub fn skin_meta_path(uuid: &str) -> PathsResult<PathBuf> {
Ok(skins_dir()?.join(format!("{uuid}.json")))
}
pub fn java_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("java"))
}
pub fn authlib_injector_path() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("authlib-injector.jar"))
}
pub fn clusters_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("clusters"))
}
pub fn cluster_trash_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join(".trash"))
}
pub fn shared_minecraft_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join(".minecraft"))
}
/// Presence marks a cluster folder as its own game dir instead of the shared `.minecraft`
pub const DEDICATED_MARKER: &str = ".dedicated_directory";
pub const INSTANCE_FILE: &str = ".instance.json";
pub fn cluster_dir(folder_name: &str) -> PathsResult<PathBuf> {
Ok(clusters_dir()?.join(folder_name))
}
pub fn cluster_uses_dedicated_dir(folder_name: &str) -> bool {
cluster_dir(folder_name).is_ok_and(|dir| dir.join(DEDICATED_MARKER).exists())
}
/// Lives here not on `Cluster` so the content layer can resolve it from a bare
/// `ClusterRow` without duplicating the marker-file rule
pub fn cluster_game_dir(folder_name: &str, isolated: bool) -> PathsResult<PathBuf> {
if isolated || cluster_uses_dedicated_dir(folder_name) {
cluster_dir(folder_name)
} else {
shared_minecraft_dir()
}
}
pub fn cluster_mods_dir(folder_name: &str) -> PathsResult<PathBuf> {
Ok(cluster_dir(folder_name)?.join(ContentType::Mod.folder_name()))
}
pub fn shared_mods_dir() -> PathsResult<PathBuf> {
Ok(shared_minecraft_dir()?.join(ContentType::Mod.folder_name()))
}
pub fn global_content_dir(content_type: ContentType) -> PathsResult<PathBuf> {
Ok(shared_minecraft_dir()?.join(content_type.folder_name()))
}
pub fn shared_mods_link(folder_name: &str) -> PathsResult<PathBuf> {
Ok(shared_mods_dir()?.join(folder_name))
}
pub fn packages_cache_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("packages"))
}
pub fn caches_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("caches"))
}
pub fn bundles_dir() -> PathsResult<PathBuf> {
Ok(caches_dir()?.join("bundles"))
}
pub fn images_cache_dir() -> PathsResult<PathBuf> {
Ok(caches_dir()?.join("images"))
}
pub const LOCAL_IMAGE_SCHEME: &str = "local:";
pub fn local_icons_dir() -> PathsResult<PathBuf> {
Ok(images_cache_dir()?.join("local"))
}
pub fn local_image_path(reference: &str) -> Option<PathBuf> {
let name = reference.strip_prefix(LOCAL_IMAGE_SCHEME)?;
if name.is_empty() || name.contains(['/', '\\']) || name.contains("..") {
return None;
}
Some(local_icons_dir().ok()?.join(name))
}
pub fn profiles_cache_dir() -> PathsResult<PathBuf> {
Ok(caches_dir()?.join("profiles"))
}
pub fn versions_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("versions"))
}
pub fn metadata_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata"))
}
pub fn libraries_dir() -> PathsResult<PathBuf> {
Ok(metadata_dir()?.join("libraries"))
}
pub fn natives_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("natives"))
}
pub fn assets_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("assets"))
}
pub fn assets_index_dir() -> PathsResult<PathBuf> {
Ok(assets_dir()?.join("indexes"))
}
pub fn assets_object_dir() -> PathsResult<PathBuf> {
Ok(assets_dir()?.join("objects"))
}
pub fn legacy_assets_dir() -> PathsResult<PathBuf> {
Ok(data_dir()?.join("metadata").join("resources"))
}
pub fn package_version_dir(
content_type: ContentType,
provider: ProviderId,
project_id: &str,
version_id: &str,
) -> PathsResult<PathBuf> {
Ok(packages_cache_dir()?
.join(content_type.folder_name())
.join(provider.dir_name())
.join(project_id)
.join(version_id))
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::OsStr;
#[test]
fn the_launcher_sits_one_level_under_the_organization_folder() {
let organization = organization_dir().expect("a home directory");
assert_eq!(organization.file_name(), Some(OsStr::new(ORGANIZATION)));
let app = organization.join(APPLICATION);
let tail: Vec<&OsStr> = app.iter().rev().take(2).collect();
assert_eq!(
tail,
vec![OsStr::new(APPLICATION), OsStr::new(ORGANIZATION)],
"every Polyfrost product shares one folder and takes a single name inside it"
);
}
#[cfg(windows)]
#[test]
fn a_windows_install_roams_with_the_user() {
let organization = organization_dir().expect("a home directory");
assert!(
organization
.iter()
.any(|part| part == OsStr::new("Roaming")),
"settings and saves follow the user between machines; Local would strand them"
);
}
#[test]
fn a_debug_build_never_shares_a_folder_with_a_release_one() {
assert_eq!(
APPLICATION,
if cfg!(debug_assertions) {
"OneClient-dev"
} else {
"OneClient"
}
);
}
}