From 8ed0b6fb12fad93b250fc9e1db35e05adac4c89c Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 19 Feb 2026 00:14:57 +0300 Subject: [PATCH 1/5] rate-limiter. re-acquire lock after sleep to prevent use-after-free I thought rust fixed this... Signed-off-by: NotAShelf Change-Id: I3a2fe427cdc19a6317510e8736fe46d56a6a6964 --- src/rate_limiter.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/rate_limiter.rs b/src/rate_limiter.rs index 72dde8d..871c666 100644 --- a/src/rate_limiter.rs +++ b/src/rate_limiter.rs @@ -87,12 +87,14 @@ impl RateLimiter { if wait_time > Duration::ZERO { drop(inner); tokio::time::sleep(wait_time).await; + let mut inner = self.inner.lock().await; + let platform_requests = + inner.requests.entry(platform.to_string()).or_default(); + platform_requests.push(Instant::now()); + return Ok(()); } } - let mut inner = self.inner.lock().await; - let platform_requests = - inner.requests.entry(platform.to_string()).or_default(); platform_requests.push(Instant::now()); Ok(()) From fc2d89892cf9e740684a93ae6cc302f15e30eb2f Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 21 Feb 2026 19:14:45 +0300 Subject: [PATCH 2/5] model/lockfile: revert `get_project` to O(n) linear scan; remove unused project index Signed-off-by: NotAShelf Change-Id: I3a35ab4ce5dec8ce95a736440fa293fe6a6a6964 --- src/model/lockfile.rs | 109 +++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/src/model/lockfile.rs b/src/model/lockfile.rs index dcda70d..eb0a64b 100644 --- a/src/model/lockfile.rs +++ b/src/model/lockfile.rs @@ -578,6 +578,67 @@ pub struct LockFile { pub lockfile_version: u32, } +impl LockFile { + pub fn new( + target: Option, + mc_versions: Vec, + loaders: HashMap, + ) -> Self { + Self { + target, + mc_versions, + loaders, + projects: Vec::new(), + lockfile_version: LOCKFILE_VERSION, + } + } + + pub fn get_project(&self, pakku_id: &str) -> Option<&Project> { + self + .projects + .iter() + .find(|p| p.pakku_id.as_deref() == Some(pakku_id)) + } + + pub fn get_loader_names(&self) -> Vec { + self.loaders.keys().cloned().collect() + } + + pub fn remove_project(&mut self, pakku_id: &str) -> Option { + if let Some(pos) = self + .projects + .iter() + .position(|p| p.pakku_id.as_deref() == Some(pakku_id)) + { + Some(self.projects.remove(pos)) + } else { + None + } + } + + pub fn find_project(&self, pakku_id: &str) -> Option<&Project> { + self.get_project(pakku_id) + } + + pub fn find_project_mut(&mut self, pakku_id: &str) -> Option<&mut Project> { + self + .projects + .iter_mut() + .find(|p| p.pakku_id.as_deref() == Some(pakku_id)) + } + + pub fn find_project_by_platform_id( + &self, + platform: &str, + id: &str, + ) -> Option<&Project> { + self + .projects + .iter() + .find(|p| p.id.get(platform).is_some_and(|pid| pid == id)) + } +} + impl LockFile { pub fn load>(path: P) -> Result { Self::load_with_validation(path, true) @@ -720,52 +781,4 @@ impl LockFile { self.projects.push(project); self.projects.sort_by_key(super::project::Project::get_name); } - - pub fn get_project(&self, pakku_id: &str) -> Option<&Project> { - self - .projects - .iter() - .find(|p| p.pakku_id.as_deref() == Some(pakku_id)) - } - - pub fn get_loader_names(&self) -> Vec { - self.loaders.keys().cloned().collect() - } - - pub fn remove_project(&mut self, pakku_id: &str) -> Option { - if let Some(pos) = self - .projects - .iter() - .position(|p| p.pakku_id.as_deref() == Some(pakku_id)) - { - Some(self.projects.remove(pos)) - } else { - None - } - } - - pub fn find_project(&self, pakku_id: &str) -> Option<&Project> { - self - .projects - .iter() - .find(|p| p.pakku_id.as_deref() == Some(pakku_id)) - } - - pub fn find_project_mut(&mut self, pakku_id: &str) -> Option<&mut Project> { - self - .projects - .iter_mut() - .find(|p| p.pakku_id.as_deref() == Some(pakku_id)) - } - - pub fn find_project_by_platform_id( - &self, - platform: &str, - id: &str, - ) -> Option<&Project> { - self - .projects - .iter() - .find(|p| p.id.get(platform).is_some_and(|pid| pid == id)) - } } From 255220b43c0dfb73bdcccaaa1755e84a9bc8b5ae Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 21 Feb 2026 19:37:27 +0300 Subject: [PATCH 3/5] export: introduce `export_profile to reduce boilerplate; cleanup Signed-off-by: NotAShelf Change-Id: I30469f47be8df27ee2a31f1acdcd68a16a6a6964 --- src/export/profiles.rs | 112 ++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/src/export/profiles.rs b/src/export/profiles.rs index f132f07..d36a28b 100644 --- a/src/export/profiles.rs +++ b/src/export/profiles.rs @@ -6,65 +6,75 @@ pub trait ExportProfile { fn rules(&self) -> Vec>; } -pub struct CurseForgeProfile; -pub struct ModrinthProfile; -pub struct ServerPackProfile; +/// Implements [`ExportProfile`] for a unit struct with a static name and rule +/// list. +/// +/// Each rule entry is an expression evaluated as +/// `Box::new(super::rules::)`, supporting both bare unit struct names and +/// constructor calls with arguments. +/// +/// # Example +/// +/// ```ignore +/// export_profile! { +/// MyProfile => "my-profile" { +/// SomeRule, +/// AnotherRule::new("arg"), +/// } +/// } +/// ``` +macro_rules! export_profile { + ($struct:ident => $name:literal { $($rule:expr),* $(,)? }) => { + pub struct $struct; -impl ExportProfile for CurseForgeProfile { - fn name(&self) -> &'static str { - "curseforge" - } + impl ExportProfile for $struct { + fn name(&self) -> &'static str { + $name + } - fn rules(&self) -> Vec> { - vec![ - Box::new(super::rules::CopyProjectFilesRule), - Box::new(super::rules::FilterByPlatformRule), - Box::new(super::rules::MissingProjectsAsOverridesRule::new( - "curseforge", - )), - Box::new(super::rules::CopyOverridesRule), - Box::new(super::rules::CopyClientOverridesRule), - Box::new(super::rules::FilterServerOnlyRule), - Box::new(super::rules::GenerateManifestRule::curseforge()), - Box::new(super::rules::FilterNonRedistributableRule), - Box::new(super::rules::TextReplacementRule), - ] + fn rules(&self) -> Vec> { + use super::rules::*; + vec![ + $(Box::new($rule)),* + ] + } + } + }; +} + +export_profile! { + CurseForgeProfile => "curseforge" { + CopyProjectFilesRule, + FilterByPlatformRule, + MissingProjectsAsOverridesRule::new("curseforge"), + CopyOverridesRule, + CopyClientOverridesRule, + FilterServerOnlyRule, + GenerateManifestRule::curseforge(), + FilterNonRedistributableRule, + TextReplacementRule } } -impl ExportProfile for ModrinthProfile { - fn name(&self) -> &'static str { - "modrinth" - } - - fn rules(&self) -> Vec> { - vec![ - Box::new(super::rules::CopyProjectFilesRule), - Box::new(super::rules::FilterByPlatformRule), - Box::new(super::rules::MissingProjectsAsOverridesRule::new( - "modrinth", - )), - Box::new(super::rules::CopyOverridesRule), - Box::new(super::rules::CopyClientOverridesRule), - Box::new(super::rules::FilterServerOnlyRule), - Box::new(super::rules::GenerateManifestRule::modrinth()), - Box::new(super::rules::TextReplacementRule), - ] +export_profile! { + ModrinthProfile => "modrinth" { + CopyProjectFilesRule, + FilterByPlatformRule, + MissingProjectsAsOverridesRule::new("modrinth"), + CopyOverridesRule, + CopyClientOverridesRule, + FilterServerOnlyRule, + GenerateManifestRule::modrinth(), + TextReplacementRule } } -impl ExportProfile for ServerPackProfile { - fn name(&self) -> &'static str { - "serverpack" - } - - fn rules(&self) -> Vec> { - vec![ - Box::new(super::rules::CopyProjectFilesRule), - Box::new(super::rules::CopyServerOverridesRule), - Box::new(super::rules::FilterClientOnlyRule), - Box::new(super::rules::TextReplacementRule), - ] +export_profile! { + ServerPackProfile => "serverpack" { + CopyProjectFilesRule, + CopyServerOverridesRule, + FilterClientOnlyRule, + TextReplacementRule } } From 5a7762ac1328df5e9e4da0bdbdcf34808966054b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 21 Feb 2026 19:50:55 +0300 Subject: [PATCH 4/5] rate-limiter: re-validate burst window in loop after sleep Signed-off-by: NotAShelf Change-Id: Iaa63eef1f055b9a98c90739025bd3ff36a6a6964 --- src/rate_limiter.rs | 41 +++++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/rate_limiter.rs b/src/rate_limiter.rs index 871c666..f5832f8 100644 --- a/src/rate_limiter.rs +++ b/src/rate_limiter.rs @@ -72,32 +72,29 @@ impl RateLimiter { let interval = Duration::from_secs(60) / rate.max(1); - let mut inner = self.inner.lock().await; - let now = Instant::now(); - let platform_requests = - inner.requests.entry(platform.to_string()).or_default(); + loop { + let mut inner = self.inner.lock().await; + let now = Instant::now(); + let platform_requests = + inner.requests.entry(platform.to_string()).or_default(); - platform_requests - .retain(|t| now.duration_since(*t) < Duration::from_secs(60)); + platform_requests + .retain(|t| now.duration_since(*t) < Duration::from_secs(60)); - if platform_requests.len() >= burst as usize - && let Some(oldest) = platform_requests.first() - { - let wait_time = interval.saturating_sub(now.duration_since(*oldest)); - if wait_time > Duration::ZERO { - drop(inner); - tokio::time::sleep(wait_time).await; - let mut inner = self.inner.lock().await; - let platform_requests = - inner.requests.entry(platform.to_string()).or_default(); - platform_requests.push(Instant::now()); - return Ok(()); + if platform_requests.len() >= burst as usize + && let Some(oldest) = platform_requests.first() + { + let wait_time = interval.saturating_sub(now.duration_since(*oldest)); + if wait_time > Duration::ZERO { + drop(inner); + tokio::time::sleep(wait_time).await; + continue; + } } + + platform_requests.push(Instant::now()); + return Ok(()); } - - platform_requests.push(Instant::now()); - - Ok(()) } pub async fn wait_for(&self, platform: &str) { From e78317849398c18473bdeb6d44beeec5b91fb87a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 21 Feb 2026 19:55:36 +0300 Subject: [PATCH 5/5] lockfile: remove unused `new` method Signed-off-by: NotAShelf Change-Id: If64632acaf9805b68a662e25aebe60216a6a6964 --- src/model/lockfile.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/model/lockfile.rs b/src/model/lockfile.rs index eb0a64b..bc80cee 100644 --- a/src/model/lockfile.rs +++ b/src/model/lockfile.rs @@ -579,20 +579,6 @@ pub struct LockFile { } impl LockFile { - pub fn new( - target: Option, - mc_versions: Vec, - loaders: HashMap, - ) -> Self { - Self { - target, - mc_versions, - loaders, - projects: Vec::new(), - lockfile_version: LOCKFILE_VERSION, - } - } - pub fn get_project(&self, pakku_id: &str) -> Option<&Project> { self .projects