use std::path::Path;
use super::model::{Arguments, LaunchProfile};
const MAX_INHERITANCE_DEPTH: usize = 8;
#[derive(Debug, thiserror::Error)]
pub enum ResolveError {
#[error("parent profile not found at {0}")]
ParentNotFound(String),
#[error("failed to parse parent profile {0}: {1}")]
ParseError(String, String),
#[error("circular inheritance detected: {0} appears more than once in the chain")]
CircularInheritance(String),
#[error("inheritance chain exceeded {0} levels")]
DepthExceeded(usize),
#[error("I/O error reading parent profile: {0}")]
Io(#[from] std::io::Error),
}
pub fn merge_into(child: LaunchProfile, parent: LaunchProfile) -> LaunchProfile {
LaunchProfile {
id: child.id,
inherits_from: parent.inherits_from,
main_class: child.main_class.or(parent.main_class),
libraries: merge_libraries(child.libraries, parent.libraries),
arguments: merge_arguments(child.arguments, parent.arguments),
minecraft_arguments: child.minecraft_arguments.or(parent.minecraft_arguments),
asset_index: child.asset_index.or(parent.asset_index),
assets: child.assets.or(parent.assets),
java_version: child.java_version.or(parent.java_version),
downloads: child.downloads.or(parent.downloads),
release_time: child.release_time.or(parent.release_time),
time: child.time.or(parent.time),
game_arguments: None,
type_: child.type_.or(parent.type_),
}
}
fn coord_key(name: &str) -> &str {
let mut it = name.match_indices(':').map(|(i, _)| i);
it.next();
it.next().map_or(name, |i| &name[..i])
}
fn merge_libraries(
child: Vec<crate::launch_profile::model::Library>,
parent: Vec<crate::launch_profile::model::Library>,
) -> Vec<crate::launch_profile::model::Library> {
use std::collections::HashSet;
let child_keys: HashSet<&str> = child.iter().map(|l| coord_key(&l.name)).collect();
let mut out: Vec<crate::launch_profile::model::Library> = parent
.into_iter()
.filter(|l| !child_keys.contains(coord_key(&l.name)))
.collect();
out.extend(child);
out
}
fn merge_arguments(child: Option<Arguments>, parent: Option<Arguments>) -> Option<Arguments> {
match (child, parent) {
(None, None) => None,
(Some(c), None) => Some(c),
(None, Some(p)) => Some(p),
(Some(c), Some(p)) => {
let mut game = p.game;
game.extend(c.game);
let mut jvm = p.jvm;
jvm.extend(c.jvm);
Some(Arguments { game, jvm })
}
}
}
pub async fn resolve(
profile: LaunchProfile,
meta_dir: &Path,
) -> Result<LaunchProfile, ResolveError> {
use std::collections::HashSet;
let mut visited: HashSet<String> = HashSet::new();
visited.insert(profile.id.clone());
let mut current = profile;
let mut depth = 0;
while let Some(parent_id) = current.inherits_from.clone() {
depth += 1;
if depth > MAX_INHERITANCE_DEPTH {
return Err(ResolveError::DepthExceeded(MAX_INHERITANCE_DEPTH));
}
if !visited.insert(parent_id.clone()) {
return Err(ResolveError::CircularInheritance(parent_id));
}
let parent_path = crate::storage::MetadataPaths::new(meta_dir)
.versions()
.join(&parent_id)
.join("meta.json");
if !parent_path.exists() {
return Err(ResolveError::ParentNotFound(
parent_path.display().to_string(),
));
}
let parent_bytes = tokio::fs::read(&parent_path).await?;
let parent: LaunchProfile = serde_json::from_slice(&parent_bytes)
.map_err(|e| ResolveError::ParseError(parent_id.clone(), e.to_string()))?;
current = merge_into(current, parent);
}
current.inherits_from = None;
Ok(current)
}
#[cfg(test)]
#[path = "tests/resolve.rs"]
mod tests;