aria2_core/engine/
active_output_registry.rs1use std::collections::HashSet;
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use tokio::sync::RwLock;
12use tracing::{debug, info, warn};
13
14pub struct ActiveOutputRegistry {
20 inner: Arc<RwLock<HashSet<PathBuf>>>,
21}
22
23impl Default for ActiveOutputRegistry {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl ActiveOutputRegistry {
30 pub fn new() -> Self {
32 Self {
33 inner: Arc::new(RwLock::new(HashSet::new())),
34 }
35 }
36
37 pub async fn resolve(&self, desired: &Path) -> PathBuf {
48 let mut registry = self.inner.write().await;
49
50 if !registry.contains(desired) {
51 registry.insert(desired.to_path_buf());
52 debug!(
53 "Output path registered (no conflict): {}",
54 desired.display()
55 );
56 return desired.to_path_buf();
57 }
58
59 let stem = desired
61 .file_stem()
62 .map(|s| s.to_string_lossy().into_owned())
63 .unwrap_or_default();
64
65 let ext = desired
66 .extension()
67 .map(|e| format!(".{}", e.to_string_lossy()))
68 .unwrap_or_default();
69
70 let parent = desired.parent().unwrap_or_else(|| Path::new("."));
71
72 let mut counter: u32 = 1;
73 loop {
74 let candidate = parent.join(format!("{} ({}){}", stem, counter, ext));
75
76 if !registry.contains(&candidate) && !candidate.exists() {
79 registry.insert(candidate.clone());
80 warn!(
81 "Filename collision: '{}' is already in use. Resolved to '{}'",
82 desired.display(),
83 candidate.display()
84 );
85 return candidate;
86 }
87
88 counter += 1;
89
90 if counter > 10_000 {
92 let fallback = parent.join(format!("{}_collision_{}", stem, counter));
93 registry.insert(fallback.clone());
94 warn!(
95 "Exhausted normal suffix range for '{}', using fallback '{}'",
96 desired.display(),
97 fallback.display()
98 );
99 return fallback;
100 }
101 }
102 }
103
104 pub async fn release(&self, path: &Path) {
109 let mut registry = self.inner.write().await;
110 if registry.remove(path) {
111 debug!("Output path released: {}", path.display());
112 }
113 }
114
115 pub async fn len(&self) -> usize {
117 self.inner.read().await.len()
118 }
119
120 pub async fn is_empty(&self) -> bool {
122 self.inner.read().await.is_empty()
123 }
124}
125
126use std::sync::OnceLock;
132
133static GLOBAL_REGISTRY: OnceLock<ActiveOutputRegistry> = OnceLock::new();
135
136pub fn global_registry() -> &'static ActiveOutputRegistry {
138 GLOBAL_REGISTRY.get_or_init(|| {
139 info!("Global ActiveOutputRegistry initialized");
140 ActiveOutputRegistry::new()
141 })
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[tokio::test]
149 async fn test_no_collision_when_unique() {
150 let reg = ActiveOutputRegistry::new();
151 let p1 = reg.resolve(Path::new("/tmp/file.txt")).await;
152 assert_eq!(p1, PathBuf::from("/tmp/file.txt"));
153 assert_eq!(reg.len().await, 1);
154 }
155
156 #[tokio::test]
157 async fn test_collision_generates_suffix() {
158 let reg = ActiveOutputRegistry::new();
159
160 let p1 = reg.resolve(Path::new("/tmp/file.txt")).await;
161 let p2 = reg.resolve(Path::new("/tmp/file.txt")).await;
162
163 assert_eq!(p1, PathBuf::from("/tmp/file.txt"));
164 assert_eq!(p2, PathBuf::from("/tmp/file (1).txt"));
165 assert_eq!(reg.len().await, 2);
166 }
167
168 #[tokio::test]
169 async fn test_triple_collision() {
170 let reg = ActiveOutputRegistry::new();
171
172 let _p1 = reg.resolve(Path::new("/tmp/data.bin")).await;
173 let _p2 = reg.resolve(Path::new("/tmp/data.bin")).await;
174 let p3 = reg.resolve(Path::new("/tmp/data.bin")).await;
175
176 assert_eq!(p3, PathBuf::from("/tmp/data (2).bin"));
177 }
178
179 #[tokio::test]
180 async fn test_release_allows_reuse() {
181 let reg = ActiveOutputRegistry::new();
182
183 let p1 = reg.resolve(Path::new("/tmp/reusable.txt")).await;
184 reg.release(&p1).await;
185 assert!(reg.is_empty().await);
186
187 let p2 = reg.resolve(Path::new("/tmp/reusable.txt")).await;
189 assert_eq!(p2, PathBuf::from("/tmp/reusable.txt"));
190 }
191
192 #[tokio::test]
193 async fn test_no_extension_handling() {
194 let reg = ActiveOutputRegistry::new();
195
196 let p1 = reg.resolve(Path::new("/tmp/Makefile")).await;
197 let p2 = reg.resolve(Path::new("/tmp/Makefile")).await;
198
199 assert_eq!(p1, PathBuf::from("/tmp/Makefile"));
200 assert_eq!(p2, PathBuf::from("/tmp/Makefile (1)"));
202 }
203
204 #[tokio::test]
205 async fn test_global_singleton() {
206 let r1 = global_registry();
207 let r2 = global_registry();
208 assert!(Arc::ptr_eq(&r1.inner, &r2.inner));
210 }
211}