Compare commits

...

5 commits

Author SHA1 Message Date
e783178493
lockfile: remove unused new method
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If64632acaf9805b68a662e25aebe60216a6a6964
2026-02-21 19:56:05 +03:00
5a7762ac13
rate-limiter: re-validate burst window in loop after sleep
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iaa63eef1f055b9a98c90739025bd3ff36a6a6964
2026-02-21 19:56:04 +03:00
255220b43c
export: introduce `export_profile to reduce boilerplate; cleanup
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I30469f47be8df27ee2a31f1acdcd68a16a6a6964
2026-02-21 19:56:03 +03:00
fc2d89892c
model/lockfile: revert get_project to O(n) linear scan; remove unused project index
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3a35ab4ce5dec8ce95a736440fa293fe6a6a6964
2026-02-21 19:56:02 +03:00
8ed0b6fb12
rate-limiter. re-acquire lock after sleep to prevent use-after-free
I thought rust fixed this...

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I3a2fe427cdc19a6317510e8736fe46d56a6a6964
2026-02-21 19:55:48 +03:00
3 changed files with 128 additions and 120 deletions

View file

@ -6,65 +6,75 @@ pub trait ExportProfile {
fn rules(&self) -> Vec<Box<dyn Rule>>; fn rules(&self) -> Vec<Box<dyn Rule>>;
} }
pub struct CurseForgeProfile; /// Implements [`ExportProfile`] for a unit struct with a static name and rule
pub struct ModrinthProfile; /// list.
pub struct ServerPackProfile; ///
/// Each rule entry is an expression evaluated as
/// `Box::new(super::rules::<expr>)`, 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 { impl ExportProfile for $struct {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"curseforge" $name
} }
fn rules(&self) -> Vec<Box<dyn Rule>> { fn rules(&self) -> Vec<Box<dyn Rule>> {
vec![ use super::rules::*;
Box::new(super::rules::CopyProjectFilesRule), vec![
Box::new(super::rules::FilterByPlatformRule), $(Box::new($rule)),*
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()), export_profile! {
Box::new(super::rules::FilterNonRedistributableRule), CurseForgeProfile => "curseforge" {
Box::new(super::rules::TextReplacementRule), CopyProjectFilesRule,
] FilterByPlatformRule,
MissingProjectsAsOverridesRule::new("curseforge"),
CopyOverridesRule,
CopyClientOverridesRule,
FilterServerOnlyRule,
GenerateManifestRule::curseforge(),
FilterNonRedistributableRule,
TextReplacementRule
} }
} }
impl ExportProfile for ModrinthProfile { export_profile! {
fn name(&self) -> &'static str { ModrinthProfile => "modrinth" {
"modrinth" CopyProjectFilesRule,
} FilterByPlatformRule,
MissingProjectsAsOverridesRule::new("modrinth"),
fn rules(&self) -> Vec<Box<dyn Rule>> { CopyOverridesRule,
vec![ CopyClientOverridesRule,
Box::new(super::rules::CopyProjectFilesRule), FilterServerOnlyRule,
Box::new(super::rules::FilterByPlatformRule), GenerateManifestRule::modrinth(),
Box::new(super::rules::MissingProjectsAsOverridesRule::new( TextReplacementRule
"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),
]
} }
} }
impl ExportProfile for ServerPackProfile { export_profile! {
fn name(&self) -> &'static str { ServerPackProfile => "serverpack" {
"serverpack" CopyProjectFilesRule,
} CopyServerOverridesRule,
FilterClientOnlyRule,
fn rules(&self) -> Vec<Box<dyn Rule>> { TextReplacementRule
vec![
Box::new(super::rules::CopyProjectFilesRule),
Box::new(super::rules::CopyServerOverridesRule),
Box::new(super::rules::FilterClientOnlyRule),
Box::new(super::rules::TextReplacementRule),
]
} }
} }

View file

@ -578,6 +578,53 @@ pub struct LockFile {
pub lockfile_version: u32, pub lockfile_version: u32,
} }
impl LockFile {
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<String> {
self.loaders.keys().cloned().collect()
}
pub fn remove_project(&mut self, pakku_id: &str) -> Option<Project> {
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 { impl LockFile {
pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> { pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load_with_validation(path, true) Self::load_with_validation(path, true)
@ -720,52 +767,4 @@ impl LockFile {
self.projects.push(project); self.projects.push(project);
self.projects.sort_by_key(super::project::Project::get_name); 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<String> {
self.loaders.keys().cloned().collect()
}
pub fn remove_project(&mut self, pakku_id: &str) -> Option<Project> {
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))
}
} }

View file

@ -72,30 +72,29 @@ impl RateLimiter {
let interval = Duration::from_secs(60) / rate.max(1); let interval = Duration::from_secs(60) / rate.max(1);
let mut inner = self.inner.lock().await; loop {
let now = Instant::now(); let mut inner = self.inner.lock().await;
let platform_requests = let now = Instant::now();
inner.requests.entry(platform.to_string()).or_default(); let platform_requests =
inner.requests.entry(platform.to_string()).or_default();
platform_requests platform_requests
.retain(|t| now.duration_since(*t) < Duration::from_secs(60)); .retain(|t| now.duration_since(*t) < Duration::from_secs(60));
if platform_requests.len() >= burst as usize if platform_requests.len() >= burst as usize
&& let Some(oldest) = platform_requests.first() && let Some(oldest) = platform_requests.first()
{ {
let wait_time = interval.saturating_sub(now.duration_since(*oldest)); let wait_time = interval.saturating_sub(now.duration_since(*oldest));
if wait_time > Duration::ZERO { if wait_time > Duration::ZERO {
drop(inner); drop(inner);
tokio::time::sleep(wait_time).await; tokio::time::sleep(wait_time).await;
continue;
}
} }
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(())
} }
pub async fn wait_for(&self, platform: &str) { pub async fn wait_for(&self, platform: &str) {