aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src-tauri/src/core
diff options
context:
space:
mode:
author苏向夜 <46275354+fu050409@users.noreply.github.com>2026-01-19 11:06:38 +0800
committerGitHub <noreply@github.com>2026-01-19 11:06:38 +0800
commitf5560d7e8abe4a41c5f959cb6eb888f6aef6ca65 (patch)
treef3675bdb552a79ddb4601ccf2f5ddd81eb47c9fb /src-tauri/src/core
parentee767338d6db510ef15d6b8cc11f6fb9a6215a43 (diff)
parentbdff2175a8470accdab030b3931406495c56074d (diff)
downloadDropOut-f5560d7e8abe4a41c5f959cb6eb888f6aef6ca65.tar.gz
DropOut-f5560d7e8abe4a41c5f959cb6eb888f6aef6ca65.zip
Merge branch 'main' into chore/migrate-repository
Diffstat (limited to 'src-tauri/src/core')
-rw-r--r--src-tauri/src/core/auth.rs5
-rw-r--r--src-tauri/src/core/config.rs36
-rw-r--r--src-tauri/src/core/downloader.rs22
-rw-r--r--src-tauri/src/core/instance.rs279
-rw-r--r--src-tauri/src/core/java.rs20
-rw-r--r--src-tauri/src/core/manifest.rs37
-rw-r--r--src-tauri/src/core/rules.rs88
7 files changed, 448 insertions, 39 deletions
diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs
index ac5904c..d5e6c17 100644
--- a/src-tauri/src/core/auth.rs
+++ b/src-tauri/src/core/auth.rs
@@ -6,9 +6,9 @@ use uuid::Uuid;
// This is critical because Microsoft's WAF often blocks requests without a valid UA
fn get_client() -> reqwest::Client {
reqwest::Client::builder()
- .user_agent("DropOut/1.0 (Linux)")
+ .user_agent("DropOut/1.0")
.build()
- .unwrap_or_else(|_| get_client())
+ .unwrap_or_else(|_| reqwest::Client::new())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -136,7 +136,6 @@ pub async fn refresh_microsoft_token(refresh_token: &str) -> Result<TokenRespons
}
/// Check if a Microsoft account token is expired or about to expire
-#[allow(dead_code)]
pub fn is_token_expired(expires_at: i64) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
diff --git a/src-tauri/src/core/config.rs b/src-tauri/src/core/config.rs
index 4c4acad..e4b9381 100644
--- a/src-tauri/src/core/config.rs
+++ b/src-tauri/src/core/config.rs
@@ -42,6 +42,34 @@ impl Default for AssistantConfig {
}
}
+/// Feature-gated arguments configuration
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct FeatureFlags {
+ /// Demo user: enables demo-related arguments when rules require it
+ pub demo_user: bool,
+ /// Quick Play: enable quick play arguments
+ pub quick_play_enabled: bool,
+ /// Quick Play singleplayer world path (if provided)
+ pub quick_play_path: Option<String>,
+ /// Quick Play singleplayer flag
+ pub quick_play_singleplayer: bool,
+ /// Quick Play multiplayer server address (optional)
+ pub quick_play_multiplayer_server: Option<String>,
+}
+
+impl Default for FeatureFlags {
+ fn default() -> Self {
+ Self {
+ demo_user: false,
+ quick_play_enabled: false,
+ quick_play_path: None,
+ quick_play_singleplayer: true,
+ quick_play_multiplayer_server: None,
+ }
+ }
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct LauncherConfig {
@@ -59,6 +87,11 @@ pub struct LauncherConfig {
pub log_upload_service: String, // "paste.rs" or "pastebin.com"
pub pastebin_api_key: Option<String>,
pub assistant: AssistantConfig,
+ // Storage management
+ pub use_shared_caches: bool, // Use global shared versions/libraries/assets
+ pub keep_legacy_per_instance_storage: bool, // Keep old per-instance caches (no migration)
+ // Feature-gated argument flags
+ pub feature_flags: FeatureFlags,
}
impl Default for LauncherConfig {
@@ -78,6 +111,9 @@ impl Default for LauncherConfig {
log_upload_service: "paste.rs".to_string(),
pastebin_api_key: None,
assistant: AssistantConfig::default(),
+ use_shared_caches: false,
+ keep_legacy_per_instance_storage: true,
+ feature_flags: FeatureFlags::default(),
}
}
}
diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs
index 9c6b7f0..26f6ebd 100644
--- a/src-tauri/src/core/downloader.rs
+++ b/src-tauri/src/core/downloader.rs
@@ -270,12 +270,12 @@ pub async fn download_with_resume(
}
current_pos += chunk_len;
- let total_downloaded = progress.fetch_add(chunk_len, Ordering::Relaxed) + chunk_len;
+ let total_downloaded = progress.fetch_add(chunk_len, Ordering::AcqRel) + chunk_len;
// Emit progress event (throttled)
- let last_bytes = last_progress_bytes.load(Ordering::Relaxed);
+ let last_bytes = last_progress_bytes.load(Ordering::Acquire);
if total_downloaded - last_bytes > 100 * 1024 || total_downloaded >= total_size {
- last_progress_bytes.store(total_downloaded, Ordering::Relaxed);
+ last_progress_bytes.store(total_downloaded, Ordering::Release);
let elapsed = start_time.elapsed().as_secs_f64();
let speed = if elapsed > 0.0 {
@@ -319,7 +319,7 @@ pub async fn download_with_resume(
all_success = false;
if e.contains("cancelled") {
// Save progress for resume
- metadata.downloaded_bytes = progress.load(Ordering::Relaxed);
+ metadata.downloaded_bytes = progress.load(Ordering::Acquire);
let meta_content =
serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?;
tokio::fs::write(&meta_path, meta_content).await.ok();
@@ -335,7 +335,7 @@ pub async fn download_with_resume(
if !all_success {
// Save progress
- metadata.downloaded_bytes = progress.load(Ordering::Relaxed);
+ metadata.downloaded_bytes = progress.load(Ordering::Acquire);
let meta_content = serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?;
tokio::fs::write(&meta_path, meta_content).await.ok();
return Err("Some segments failed".to_string());
@@ -482,19 +482,19 @@ impl GlobalProgress {
/// Get current progress snapshot without modification
fn snapshot(&self) -> ProgressSnapshot {
ProgressSnapshot {
- completed_files: self.completed_files.load(Ordering::Relaxed),
+ completed_files: self.completed_files.load(Ordering::Acquire),
total_files: self.total_files,
- total_downloaded_bytes: self.total_downloaded_bytes.load(Ordering::Relaxed),
+ total_downloaded_bytes: self.total_downloaded_bytes.load(Ordering::Acquire),
}
}
/// Increment completed files counter and return updated snapshot
fn inc_completed(&self) -> ProgressSnapshot {
- let completed = self.completed_files.fetch_add(1, Ordering::Relaxed) + 1;
+ let completed = self.completed_files.fetch_add(1, Ordering::Release) + 1;
ProgressSnapshot {
completed_files: completed,
total_files: self.total_files,
- total_downloaded_bytes: self.total_downloaded_bytes.load(Ordering::Relaxed),
+ total_downloaded_bytes: self.total_downloaded_bytes.load(Ordering::Acquire),
}
}
@@ -502,10 +502,10 @@ impl GlobalProgress {
fn add_bytes(&self, delta: u64) -> ProgressSnapshot {
let total_bytes = self
.total_downloaded_bytes
- .fetch_add(delta, Ordering::Relaxed)
+ .fetch_add(delta, Ordering::AcqRel)
+ delta;
ProgressSnapshot {
- completed_files: self.completed_files.load(Ordering::Relaxed),
+ completed_files: self.completed_files.load(Ordering::Acquire),
total_files: self.total_files,
total_downloaded_bytes: total_bytes,
}
diff --git a/src-tauri/src/core/instance.rs b/src-tauri/src/core/instance.rs
index 90ec34e..573273e 100644
--- a/src-tauri/src/core/instance.rs
+++ b/src-tauri/src/core/instance.rs
@@ -6,6 +6,7 @@
//! - Support for instance switching and isolation
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
@@ -24,6 +25,16 @@ pub struct Instance {
pub notes: Option<String>, // 备注(可选)
pub mod_loader: Option<String>, // 模组加载器类型:"fabric", "forge", "vanilla"
pub mod_loader_version: Option<String>, // 模组加载器版本
+ pub jvm_args_override: Option<String>, // JVM参数覆盖(可选)
+ #[serde(default)]
+ pub memory_override: Option<MemoryOverride>, // 内存设置覆盖(可选)
+}
+
+/// Memory settings override for an instance
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MemoryOverride {
+ pub min: u32, // MB
+ pub max: u32, // MB
}
/// Configuration for all instances
@@ -98,6 +109,8 @@ impl InstanceState {
notes: None,
mod_loader: Some("vanilla".to_string()),
mod_loader_version: None,
+ jvm_args_override: None,
+ memory_override: None,
};
let mut config = self.instances.lock().unwrap();
@@ -218,21 +231,44 @@ impl InstanceState {
.get_instance(id)
.ok_or_else(|| format!("Instance {} not found", id))?;
- // Create new instance
- let mut new_instance = self.create_instance(new_name, app_handle)?;
+ // Prepare new instance metadata (but don't save yet)
+ let new_id = uuid::Uuid::new_v4().to_string();
+ let instances_dir = app_handle
+ .path()
+ .app_data_dir()
+ .map_err(|e| e.to_string())?
+ .join("instances");
+ let new_game_dir = instances_dir.join(&new_id);
- // Copy instance properties
- new_instance.version_id = source_instance.version_id.clone();
- new_instance.mod_loader = source_instance.mod_loader.clone();
- new_instance.mod_loader_version = source_instance.mod_loader_version.clone();
- new_instance.notes = source_instance.notes.clone();
-
- // Copy directory contents
+ // Copy directory FIRST - if this fails, don't create metadata
if source_instance.game_dir.exists() {
- copy_dir_all(&source_instance.game_dir, &new_instance.game_dir)
+ copy_dir_all(&source_instance.game_dir, &new_game_dir)
.map_err(|e| format!("Failed to copy instance directory: {}", e))?;
+ } else {
+ // If source dir doesn't exist, create new empty game dir
+ std::fs::create_dir_all(&new_game_dir)
+ .map_err(|e| format!("Failed to create instance directory: {}", e))?;
}
+ // NOW create metadata and save
+ let new_instance = Instance {
+ id: new_id,
+ name: new_name,
+ game_dir: new_game_dir,
+ version_id: source_instance.version_id.clone(),
+ mod_loader: source_instance.mod_loader.clone(),
+ mod_loader_version: source_instance.mod_loader_version.clone(),
+ notes: source_instance.notes.clone(),
+ icon_path: source_instance.icon_path.clone(),
+ created_at: std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_secs() as i64,
+ last_played: None,
+ jvm_args_override: source_instance.jvm_args_override.clone(),
+ memory_override: source_instance.memory_override.clone(),
+ };
+
self.update_instance(new_instance.clone())?;
Ok(new_instance)
@@ -323,3 +359,226 @@ pub fn migrate_legacy_data(
Ok(())
}
+
+/// Migrate instance caches to shared global caches
+///
+/// This function deduplicates versions, libraries, and assets from all instances
+/// into a global shared cache. It prefers hard links (instant, zero-copy) and
+/// falls back to copying if hard links are not supported.
+///
+/// # Arguments
+/// * `app_handle` - Tauri app handle
+/// * `instance_state` - Instance state management
+///
+/// # Returns
+/// * `Ok((moved_count, hardlink_count, copy_count, saved_bytes))` on success
+/// * `Err(String)` on failure
+pub fn migrate_to_shared_caches(
+ app_handle: &AppHandle,
+ instance_state: &InstanceState,
+) -> Result<(usize, usize, usize, u64), String> {
+ let app_dir = app_handle.path().app_data_dir().unwrap();
+
+ // Global shared cache directories
+ let global_versions = app_dir.join("versions");
+ let global_libraries = app_dir.join("libraries");
+ let global_assets = app_dir.join("assets");
+
+ // Create global cache directories
+ std::fs::create_dir_all(&global_versions).map_err(|e| e.to_string())?;
+ std::fs::create_dir_all(&global_libraries).map_err(|e| e.to_string())?;
+ std::fs::create_dir_all(&global_assets).map_err(|e| e.to_string())?;
+
+ let mut total_moved = 0;
+ let mut hardlink_count = 0;
+ let mut copy_count = 0;
+ let mut saved_bytes = 0u64;
+
+ // Get all instances
+ let instances = instance_state.list_instances();
+
+ for instance in instances {
+ let instance_versions = instance.game_dir.join("versions");
+ let instance_libraries = instance.game_dir.join("libraries");
+ let instance_assets = instance.game_dir.join("assets");
+
+ // Migrate versions
+ if instance_versions.exists() {
+ let (moved, hardlinks, copies, bytes) =
+ deduplicate_directory(&instance_versions, &global_versions)?;
+ total_moved += moved;
+ hardlink_count += hardlinks;
+ copy_count += copies;
+ saved_bytes += bytes;
+ }
+
+ // Migrate libraries
+ if instance_libraries.exists() {
+ let (moved, hardlinks, copies, bytes) =
+ deduplicate_directory(&instance_libraries, &global_libraries)?;
+ total_moved += moved;
+ hardlink_count += hardlinks;
+ copy_count += copies;
+ saved_bytes += bytes;
+ }
+
+ // Migrate assets
+ if instance_assets.exists() {
+ let (moved, hardlinks, copies, bytes) =
+ deduplicate_directory(&instance_assets, &global_assets)?;
+ total_moved += moved;
+ hardlink_count += hardlinks;
+ copy_count += copies;
+ saved_bytes += bytes;
+ }
+ }
+
+ Ok((total_moved, hardlink_count, copy_count, saved_bytes))
+}
+
+/// Deduplicate a directory tree into a global cache
+///
+/// Recursively processes all files, checking SHA1 hashes for deduplication.
+/// Returns (total_moved, hardlink_count, copy_count, saved_bytes)
+fn deduplicate_directory(
+ source_dir: &Path,
+ dest_dir: &Path,
+) -> Result<(usize, usize, usize, u64), String> {
+ let mut moved = 0;
+ let mut hardlinks = 0;
+ let mut copies = 0;
+ let mut saved_bytes = 0u64;
+
+ // Build a hash map of existing files in dest (hash -> path)
+ let mut dest_hashes: HashMap<String, PathBuf> = HashMap::new();
+ if dest_dir.exists() {
+ index_directory_hashes(dest_dir, dest_dir, &mut dest_hashes)?;
+ }
+
+ // Process source directory
+ process_directory_for_migration(
+ source_dir,
+ source_dir,
+ dest_dir,
+ &dest_hashes,
+ &mut moved,
+ &mut hardlinks,
+ &mut copies,
+ &mut saved_bytes,
+ )?;
+
+ Ok((moved, hardlinks, copies, saved_bytes))
+}
+
+/// Index all files in a directory by their SHA1 hash
+fn index_directory_hashes(
+ dir: &Path,
+ base: &Path,
+ hashes: &mut HashMap<String, PathBuf>,
+) -> Result<(), String> {
+ if !dir.is_dir() {
+ return Ok(());
+ }
+
+ for entry in std::fs::read_dir(dir).map_err(|e| e.to_string())? {
+ let entry = entry.map_err(|e| e.to_string())?;
+ let path = entry.path();
+
+ if path.is_dir() {
+ index_directory_hashes(&path, base, hashes)?;
+ } else if path.is_file() {
+ let hash = compute_file_sha1(&path)?;
+ hashes.insert(hash, path);
+ }
+ }
+
+ Ok(())
+}
+
+/// Process directory for migration (recursive)
+fn process_directory_for_migration(
+ current: &Path,
+ source_base: &Path,
+ dest_base: &Path,
+ dest_hashes: &HashMap<String, PathBuf>,
+ moved: &mut usize,
+ hardlinks: &mut usize,
+ copies: &mut usize,
+ saved_bytes: &mut u64,
+) -> Result<(), String> {
+ if !current.is_dir() {
+ return Ok(());
+ }
+
+ for entry in std::fs::read_dir(current).map_err(|e| e.to_string())? {
+ let entry = entry.map_err(|e| e.to_string())?;
+ let source_path = entry.path();
+
+ // Compute relative path
+ let rel_path = source_path
+ .strip_prefix(source_base)
+ .map_err(|e| e.to_string())?;
+ let dest_path = dest_base.join(rel_path);
+
+ if source_path.is_dir() {
+ // Recurse into subdirectory
+ process_directory_for_migration(
+ &source_path,
+ source_base,
+ dest_base,
+ dest_hashes,
+ moved,
+ hardlinks,
+ copies,
+ saved_bytes,
+ )?;
+ } else if source_path.is_file() {
+ let file_size = std::fs::metadata(&source_path)
+ .map(|m| m.len())
+ .unwrap_or(0);
+
+ // Compute file hash
+ let source_hash = compute_file_sha1(&source_path)?;
+
+ // Check if file already exists in dest with same hash
+ if let Some(_existing) = dest_hashes.get(&source_hash) {
+ // File exists, delete source (already deduplicated)
+ std::fs::remove_file(&source_path).map_err(|e| e.to_string())?;
+ *saved_bytes += file_size;
+ *moved += 1;
+ } else {
+ // File doesn't exist, move it
+ // Create parent directory in dest
+ if let Some(parent) = dest_path.parent() {
+ std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
+ }
+
+ // Try hard link first
+ if std::fs::hard_link(&source_path, &dest_path).is_ok() {
+ // Hard link succeeded, remove source
+ std::fs::remove_file(&source_path).map_err(|e| e.to_string())?;
+ *hardlinks += 1;
+ *moved += 1;
+ } else {
+ // Hard link failed (different filesystem?), copy instead
+ std::fs::copy(&source_path, &dest_path).map_err(|e| e.to_string())?;
+ std::fs::remove_file(&source_path).map_err(|e| e.to_string())?;
+ *copies += 1;
+ *moved += 1;
+ }
+ }
+ }
+ }
+
+ Ok(())
+}
+
+/// Compute SHA1 hash of a file
+fn compute_file_sha1(path: &Path) -> Result<String, String> {
+ use sha1::{Digest, Sha1};
+
+ let data = std::fs::read(path).map_err(|e| e.to_string())?;
+ let mut hasher = Sha1::new();
+ hasher.update(&data);
+ Ok(hex::encode(hasher.finalize()))
+}
diff --git a/src-tauri/src/core/java.rs b/src-tauri/src/core/java.rs
index 0c7769b..2e3c8a7 100644
--- a/src-tauri/src/core/java.rs
+++ b/src-tauri/src/core/java.rs
@@ -850,8 +850,24 @@ fn parse_version_string(output: &str) -> Option<String> {
/// Parse version for comparison (returns major version number)
fn parse_java_version(version: &str) -> u32 {
- // Handle both old format (1.8.0_xxx) and new format (11.0.x, 17.0.x)
- let parts: Vec<&str> = version.split('.').collect();
+ // Handle various formats:
+ // - Old format: 1.8.0_xxx (Java 8 with update)
+ // - New format: 17.0.1, 11.0.5+10 (Java 11+)
+ // - Format with build: 21.0.3+13-Ubuntu-0ubuntu0.24.04.1
+ // - Format with underscores: 1.8.0_411
+
+ // First, strip build metadata (everything after '+')
+ let version_only = version.split('+').next().unwrap_or(version);
+
+ // Remove trailing junk (like "-Ubuntu-0ubuntu0.24.04.1")
+ let version_only = version_only.split('-').next().unwrap_or(version_only);
+
+ // Replace underscores with dots (1.8.0_411 -> 1.8.0.411)
+ let normalized = version_only.replace('_', ".");
+
+ // Split by dots
+ let parts: Vec<&str> = normalized.split('.').collect();
+
if let Some(first) = parts.first() {
if *first == "1" {
// Old format: 1.8.0 -> major is 8
diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs
index 637b935..e792071 100644
--- a/src-tauri/src/core/manifest.rs
+++ b/src-tauri/src/core/manifest.rs
@@ -97,6 +97,43 @@ pub async fn fetch_vanilla_version(
Ok(resp)
}
+/// Find the root vanilla version by following the inheritance chain.
+///
+/// For modded versions (Fabric, Forge), this walks up the `inheritsFrom`
+/// chain to find the base vanilla Minecraft version.
+///
+/// # Arguments
+/// * `game_dir` - The .minecraft directory path
+/// * `version_id` - The version ID to start from
+///
+/// # Returns
+/// The ID of the root vanilla version (the version without `inheritsFrom`)
+pub async fn find_root_version(
+ game_dir: &std::path::Path,
+ version_id: &str,
+) -> Result<String, Box<dyn Error + Send + Sync>> {
+ let mut current_id = version_id.to_string();
+
+ // Keep following the inheritance chain
+ loop {
+ let version = match load_local_version(game_dir, &current_id).await {
+ Ok(v) => v,
+ Err(_) => {
+ // If not found locally, assume it's a vanilla version (root)
+ return Ok(current_id);
+ }
+ };
+
+ // If this version has no parent, it's the root
+ if let Some(parent_id) = version.inherits_from {
+ current_id = parent_id;
+ } else {
+ // This is the root
+ return Ok(current_id);
+ }
+ }
+}
+
/// Load a version, checking local first, then fetching from remote if needed.
///
/// For modded versions (those with `inheritsFrom`), this will also resolve
diff --git a/src-tauri/src/core/rules.rs b/src-tauri/src/core/rules.rs
index 71abda5..781515a 100644
--- a/src-tauri/src/core/rules.rs
+++ b/src-tauri/src/core/rules.rs
@@ -1,7 +1,8 @@
+use crate::core::config::FeatureFlags;
use crate::core::game_version::Rule;
use std::env;
-pub fn is_library_allowed(rules: &Option<Vec<Rule>>) -> bool {
+pub fn is_library_allowed(rules: &Option<Vec<Rule>>, features: Option<&FeatureFlags>) -> bool {
// If no rules, it's allowed by default
let Some(rules) = rules else {
return true;
@@ -39,36 +40,97 @@ pub fn is_library_allowed(rules: &Option<Vec<Rule>>) -> bool {
let mut allowed = false;
for rule in rules {
- if rule_matches(rule) {
+ if rule_matches(rule, features) {
allowed = rule.action == "allow";
}
}
allowed
}
-fn rule_matches(rule: &Rule) -> bool {
- // Feature-based rules (e.g., is_demo_user, has_quick_plays_support, is_quick_play_singleplayer)
- // are not implemented in this launcher, so we return false for any rule that has features.
- // This prevents adding arguments like --demo, --quickPlayPath, --quickPlaySingleplayer, etc.
- if rule.features.is_some() {
- return false;
+fn rule_matches(rule: &Rule, features: Option<&FeatureFlags>) -> bool {
+ // Feature-based rules: apply only if all listed features evaluate to true
+ if let Some(f) = &rule.features {
+ if let Some(map) = f.as_object() {
+ // If no feature flags provided, we cannot satisfy feature rules
+ let ctx = match features {
+ Some(ff) => ff,
+ None => return false,
+ };
+
+ for (key, val) in map.iter() {
+ let required = val.as_bool().unwrap_or(false);
+ // Map known features
+ let actual = match key.as_str() {
+ "is_demo_user" => ctx.demo_user,
+ "has_quick_plays_support" => ctx.quick_play_enabled,
+ "is_quick_play_singleplayer" => {
+ ctx.quick_play_enabled && ctx.quick_play_singleplayer
+ }
+ "is_quick_play_multiplayer" => {
+ ctx.quick_play_enabled
+ && ctx
+ .quick_play_multiplayer_server
+ .as_ref()
+ .map(|s| !s.is_empty())
+ .unwrap_or(false)
+ }
+ _ => false,
+ };
+ if required && !actual {
+ return false;
+ }
+ if !required && actual {
+ // If rule specifies feature must be false, but it's true, do not match
+ return false;
+ }
+ }
+ } else {
+ // Malformed features object
+ return false;
+ }
}
match &rule.os {
None => true, // No OS condition means it applies to all
Some(os_rule) => {
+ // Check OS name
if let Some(os_name) = &os_rule.name {
- match os_name.as_str() {
+ let os_match = match os_name.as_str() {
"osx" | "macos" => env::consts::OS == "macos",
"linux" => env::consts::OS == "linux",
"windows" => env::consts::OS == "windows",
_ => false, // Unknown OS name in rule
+ };
+
+ if !os_match {
+ return false;
}
- } else {
- // OS rule exists but name is None? Maybe checking version/arch only.
- // For simplicity, mostly name is used.
- true
}
+
+ // Check architecture if specified
+ if let Some(arch) = &os_rule.arch {
+ let current_arch = env::consts::ARCH;
+ // Strict match: only exact architecture or known compatibility mapping
+ let compatible = match (arch.as_str(), current_arch) {
+ ("x86_64", "x86_64") => true,
+ ("x86", "x86") => true,
+ ("aarch64", "aarch64") => true,
+ // Treat "x86" not as matching x86_64 (be strict)
+ _ => arch == current_arch,
+ };
+ if !compatible {
+ return false;
+ }
+ }
+
+ // Check version if specified (for OS version compatibility)
+ if let Some(_version) = &os_rule.version {
+ // Version checking would require parsing OS version strings
+ // For now, we accept all versions (conservative approach)
+ // In the future, parse version and compare
+ }
+
+ true
}
}
}