1use crate::{CacheConfig, Device, MemoryPolicy, Threads};
13use asc_cache::{AutoDisk, DEFAULT_MIN_DEX_BYTES, DiskCache, DiskCapacity, Tier};
14use std::collections::BTreeMap;
15use std::fmt;
16use std::path::{Path, PathBuf};
17use std::time::Duration;
18
19mod decompilers;
20
21pub use decompilers::{BUILT_IN_DECOMPILER, Decompiler};
22
23pub const CONFIG_ENV: &str = "ASC_CONFIG";
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28#[non_exhaustive]
29pub enum Source {
30 Default,
32 File,
34 Env,
36 Flag,
38}
39
40impl Source {
41 pub fn name(self) -> &'static str {
43 match self {
44 Source::Default => "default",
45 Source::File => "file",
46 Source::Env => "env",
47 Source::Flag => "flag",
48 }
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54#[non_exhaustive]
55pub enum Count {
56 Auto,
58 Fixed(usize),
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64#[non_exhaustive]
65pub enum Amount {
66 Auto,
68 Bytes(u64),
70 Percent(u8),
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub struct Bytes(pub u64);
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80#[non_exhaustive]
81pub enum CacheDir {
82 Default,
84 Path(PathBuf),
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct Tiers(pub [bool; 3]);
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct Seconds(pub Duration);
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Millis(pub Duration);
99
100#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct Name(pub String);
103
104#[derive(Debug, Clone, Default, PartialEq, Eq)]
106pub struct Names(pub Vec<String>);
107
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
110pub struct MaybePath(pub Option<PathBuf>);
111
112pub trait Value: Sized + fmt::Display {
114 const EXPECTED: &'static str;
116 fn parse(text: &str) -> Option<Self>;
118 fn is_auto(&self) -> bool {
120 false
121 }
122}
123
124const SIZE_UNITS: [(&str, u32); 5] = [("", 20), ("k", 10), ("m", 20), ("g", 30), ("t", 40)];
125
126fn parse_size(text: &str) -> Option<u64> {
128 let t = text.trim().to_ascii_lowercase();
129 let t = t.strip_suffix("ib").or_else(|| t.strip_suffix('b')).unwrap_or(&t);
130 let split = t.find(|c: char| !c.is_ascii_digit()).unwrap_or(t.len());
131 let (digits, unit) = t.split_at(split);
132 let shift = SIZE_UNITS.iter().find(|(u, _)| *u == unit.trim())?.1;
133 digits.parse::<u64>().ok()?.checked_mul(1 << shift)
134}
135
136fn show_size(bytes: u64) -> String {
137 match SIZE_UNITS[1..].iter().rev().find(|(_, s)| bytes >= 1 << s && bytes.is_multiple_of(1 << s)) {
138 Some((u, s)) => format!("{}{}", bytes >> s, u.to_ascii_uppercase()),
139 None => format!("{bytes}B"),
140 }
141}
142
143fn parse_duration(text: &str, bare: Duration) -> Option<Duration> {
144 let t = text.trim();
145 let split = t.find(|c: char| !c.is_ascii_digit()).unwrap_or(t.len());
146 let (n, unit) = t.split_at(split);
147 let n: u32 = n.parse().ok()?;
148 let unit = match unit.trim() {
149 "" => bare,
150 "ms" => Duration::from_millis(1),
151 "s" => Duration::from_secs(1),
152 "m" | "min" => Duration::from_secs(60),
153 "h" => Duration::from_secs(3600),
154 _ => return None,
155 };
156 unit.checked_mul(n)
157}
158
159fn show_duration(d: Duration) -> String {
160 let ms = d.as_millis();
161 match () {
162 _ if ms > 0 && ms.is_multiple_of(3_600_000) => format!("{}h", ms / 3_600_000),
163 _ if ms > 0 && ms.is_multiple_of(60_000) => format!("{}m", ms / 60_000),
164 _ if ms.is_multiple_of(1000) => format!("{}s", ms / 1000),
165 _ => format!("{ms}ms"),
166 }
167}
168
169impl fmt::Display for Count {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 match self {
172 Count::Auto => f.write_str("auto"),
173 Count::Fixed(n) => write!(f, "{n}"),
174 }
175 }
176}
177
178impl Value for Count {
179 const EXPECTED: &'static str = "auto or a number (0 counts as 1)";
180 fn parse(text: &str) -> Option<Count> {
181 match text.trim() {
182 "auto" => Some(Count::Auto),
183 n => n.parse().ok().map(Count::Fixed),
184 }
185 }
186 fn is_auto(&self) -> bool {
187 *self == Count::Auto
188 }
189}
190
191impl fmt::Display for Amount {
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 match self {
194 Amount::Auto => f.write_str("auto"),
195 Amount::Bytes(b) => f.write_str(&show_size(*b)),
196 Amount::Percent(p) => write!(f, "{p}%"),
197 }
198 }
199}
200
201impl Value for Amount {
202 const EXPECTED: &'static str = "auto, a size (512M, 4G; a bare number is MiB) or a percentage (25%)";
203 fn parse(text: &str) -> Option<Amount> {
204 let t = text.trim();
205 if t == "auto" {
206 return Some(Amount::Auto);
207 }
208 match t.strip_suffix('%') {
209 Some(p) => p.trim().parse().ok().filter(|p| (1..=100).contains(p)).map(Amount::Percent),
210 None => parse_size(t).map(Amount::Bytes),
211 }
212 }
213 fn is_auto(&self) -> bool {
214 *self == Amount::Auto
215 }
216}
217
218impl fmt::Display for Bytes {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 f.write_str(&show_size(self.0))
221 }
222}
223
224impl Value for Bytes {
225 const EXPECTED: &'static str = "a size (512M, 4G; a bare number is MiB)";
226 fn parse(text: &str) -> Option<Bytes> {
227 parse_size(text).map(Bytes)
228 }
229}
230
231impl Value for bool {
232 const EXPECTED: &'static str = "on or off";
233 fn parse(text: &str) -> Option<bool> {
234 match text.trim().to_ascii_lowercase().as_str() {
235 "on" | "true" | "yes" | "1" => Some(true),
236 "off" | "false" | "no" | "0" => Some(false),
237 _ => None,
238 }
239 }
240}
241
242impl fmt::Display for CacheDir {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 CacheDir::Default => f.write_str("default"),
246 CacheDir::Path(p) => write!(f, "{}", p.display()),
247 }
248 }
249}
250
251impl Value for CacheDir {
252 const EXPECTED: &'static str = "a directory, or default";
253 fn parse(text: &str) -> Option<CacheDir> {
254 match text.trim() {
255 "" => None,
256 "default" => Some(CacheDir::Default),
257 p => Some(CacheDir::Path(expand_home(p))),
258 }
259 }
260}
261
262fn expand_home(path: &str) -> PathBuf {
264 let var = |name: &str| std::env::var_os(name).filter(|v| !v.is_empty());
265 let home = var("HOME").or_else(|| if cfg!(windows) { var("USERPROFILE") } else { None });
266 match (path.strip_prefix("~/"), home) {
267 (Some(rest), Some(home)) => PathBuf::from(home).join(rest),
268 _ => PathBuf::from(path),
269 }
270}
271
272impl fmt::Display for Tiers {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 let names: Vec<_> = Tier::ALL.into_iter().filter(|t| self.0[*t as usize]).map(Tier::name).collect();
275 f.write_str(if names.is_empty() { "none".into() } else { names.join(",") }.as_str())
276 }
277}
278
279impl Value for Tiers {
280 const EXPECTED: &'static str = "a list of dex, derived, rendered (or all, none)";
281 fn parse(text: &str) -> Option<Tiers> {
282 match text.trim() {
283 "all" => return Some(Tiers([true; 3])),
284 "none" => return Some(Tiers([false; 3])),
285 _ => {}
286 }
287 let mut on = [false; 3];
288 for name in text.split(',').map(str::trim) {
289 on[*Tier::ALL.iter().find(|t| t.name() == name)? as usize] = true;
290 }
291 Some(Tiers(on))
292 }
293}
294
295impl fmt::Display for Seconds {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297 f.write_str(&show_duration(self.0))
298 }
299}
300
301impl Value for Seconds {
302 const EXPECTED: &'static str = "a duration (900, 15m; a bare number is seconds)";
303 fn parse(text: &str) -> Option<Seconds> {
304 parse_duration(text, Duration::from_secs(1)).map(Seconds)
305 }
306}
307
308impl fmt::Display for Millis {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 f.write_str(&show_duration(self.0))
311 }
312}
313
314impl Value for Millis {
315 const EXPECTED: &'static str = "a duration (300, 2s; a bare number is milliseconds)";
316 fn parse(text: &str) -> Option<Millis> {
317 parse_duration(text, Duration::from_millis(1)).map(Millis)
318 }
319}
320
321impl fmt::Display for Name {
322 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323 f.write_str(&self.0)
324 }
325}
326
327impl Value for Name {
328 const EXPECTED: &'static str = "a name";
329 fn parse(text: &str) -> Option<Name> {
330 Some(text.trim()).filter(|t| !t.is_empty() && !t.contains(',')).map(|t| Name(t.to_string()))
331 }
332}
333
334impl fmt::Display for Names {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 f.write_str(if self.0.is_empty() { "none".into() } else { self.0.join(",") }.as_str())
337 }
338}
339
340impl Value for Names {
341 const EXPECTED: &'static str = "names separated by `,` (or none)";
342 fn parse(text: &str) -> Option<Names> {
343 if matches!(text.trim(), "" | "none") {
344 return Some(Names::default());
345 }
346 text.split(',').map(|name| Name::parse(name).map(|n| n.0)).collect::<Option<_>>().map(Names)
347 }
348}
349
350impl fmt::Display for MaybePath {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 match &self.0 {
353 Some(path) => write!(f, "{}", path.display()),
354 None => f.write_str("none"),
355 }
356 }
357}
358
359impl Value for MaybePath {
360 const EXPECTED: &'static str = "a file, or none";
361 fn parse(text: &str) -> Option<MaybePath> {
362 match text.trim() {
363 "" | "none" => Some(MaybePath(None)),
364 p => Some(MaybePath(Some(expand_home(p)))),
365 }
366 }
367}
368
369#[derive(Debug, Clone, PartialEq, Eq)]
371#[non_exhaustive]
372pub struct Setting<T> {
373 pub value: T,
375 pub source: Source,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
381#[non_exhaustive]
382pub struct Key {
383 pub name: &'static str,
385 pub env: &'static str,
387 pub legacy_env: Option<&'static str>,
389 pub help: &'static str,
391}
392
393#[derive(Debug, thiserror::Error)]
395#[non_exhaustive]
396pub enum ConfigError {
397 #[error("{origin}: {key} = {text:?}: expected {expected}")]
399 Invalid {
400 origin: String,
402 key: String,
404 text: String,
406 expected: &'static str,
408 },
409 #[error("{origin}: unknown setting {key:?}; known: {known}")]
411 UnknownKey {
412 origin: String,
414 key: String,
416 known: String,
418 },
419 #[error("{path}: {message}")]
421 File {
422 path: String,
424 message: String,
426 },
427}
428
429macro_rules! settings {
432 ($($field:ident: $ty:ty = $default:expr, $name:literal, $env:literal, $legacy:expr, $help:literal;)*) => {
433 #[derive(Debug, Clone, PartialEq, Eq)]
435 #[non_exhaustive]
436 pub struct Config {
437 $(#[doc = concat!("`", $name, "`: ", $help, ".")]
438 pub $field: Setting<$ty>,)*
439 pub file: Option<PathBuf>,
441 pub decompilers: BTreeMap<String, Decompiler>,
443 }
444
445 impl Config {
446 pub const KEYS: &'static [Key] = &[$(Key { name: $name, env: $env, legacy_env: $legacy, help: $help },)*];
448
449 fn defaults() -> Config {
450 Config { $($field: Setting { value: $default, source: Source::Default },)* file: None, decompilers: BTreeMap::new() }
451 }
452
453 fn set(&mut self, name: &str, text: &str, source: Source, origin: &str) -> Result<(), ConfigError> {
455 match name {
456 $($name => self.$field = Setting { value: parse_value(name, text, origin)?, source },)*
457 _ => return Err(ConfigError::UnknownKey { origin: origin.into(), key: name.into(), known: Config::known_keys() }),
458 }
459 Ok(())
460 }
461
462 pub fn entries(&self) -> Vec<(&'static Key, String, Source, bool)> {
464 let mut keys = Config::KEYS.iter();
465 vec![$({
466 let key = keys.next().expect("one key per setting");
467 (key, self.$field.value.to_string(), self.$field.source, self.$field.value.is_auto())
468 },)*]
469 }
470 }
471 };
472}
473
474settings! {
475 threads: Count = Count::Auto, "threads", "ASC_THREADS", None,
476 "worker threads per process; auto: the CPUs, 4 per CPU split between busy asc processes";
477 memory_max: Amount = Amount::Auto, "memory.max", "ASC_MEMORY", None,
478 "memory cache of mcp and the daemon; auto = 25%: of available memory, split between resident processes";
479 memory_ceiling: Amount = Amount::Auto, "memory.ceiling", "ASC_MEMORY_CEILING", None,
480 "most one process's automatic memory cache takes (a % is of physical memory); auto: 1/16 of it, 2G..8G";
481 memory_split: bool = true, "memory.split", "ASC_MEMORY_SPLIT", None,
482 "split the automatic memory budget between the user's resident processes";
483 cache_enabled: bool = true, "cache.enabled", "ASC_CACHE", None,
484 "read and write the disk cache";
485 cache_dir: CacheDir = CacheDir::Default, "cache.dir", "ASC_CACHE_DIR", None,
486 "disk cache directory; default: $XDG_CACHE_HOME/asc, else %LOCALAPPDATA%\\asc\\cache on Windows, else ~/.cache/asc";
487 cache_max: Amount = Amount::Auto, "cache.max", "ASC_CACHE_MAX", Some("ASC_CACHE_MAX_MB"),
488 "disk cache budget; auto = 20%: of free space plus the cache, at most cache.ceiling, never into a 1 GiB / 5% reserve";
489 cache_ceiling: Bytes = Bytes(AutoDisk::CEILING), "cache.ceiling", "ASC_CACHE_CEILING", None,
490 "most the automatic disk budget takes";
491 cache_tiers: Tiers = Tiers([true; 3]), "cache.tiers", "ASC_CACHE_TIERS", None,
492 "disk cache tiers in use: dex (L1), derived (L2), rendered (L3)";
493 cache_min_dex: Bytes = Bytes(DEFAULT_MIN_DEX_BYTES), "cache.min_dex", "ASC_CACHE_MIN_DEX", Some("ASC_CACHE_MIN_DEX_MB"),
494 "smallest inflated DEX written to L1 (smaller ones are cheaper to inflate again)";
495 cache_write_queue: Bytes = Bytes(CacheConfig::WRITE_QUEUE as u64), "cache.write_queue", "ASC_CACHE_WRITE_QUEUE", None,
496 "heap queued disk writes may hold (at most the memory cache's budget); later ones inflate again when written";
497 cache_writers: Count = Count::Auto, "cache.writers", "ASC_CACHE_WRITERS", None,
498 "disk writer threads per process; auto: 1 per 8 CPUs, 1..4";
499 cache_spill: Bytes = Bytes(64 << 20), "cache.spill", "ASC_CACHE_SPILL", None,
500 "budget of out/ in the cache directory, where cut outputs go; 0 writes none";
501 daemon_idle: Seconds = Seconds(Duration::from_secs(15 * 60)), "daemon.idle", "ASC_DAEMON_IDLE", None,
502 "the daemon exits after this long without a request";
503 daemon_wait: Millis = Millis(Duration::from_millis(300)), "daemon.wait", "ASC_DAEMON_WAIT", Some("ASC_DAEMON_WAIT_MS"),
504 "how long the first command waits for the daemon it starts, then runs itself";
505 decompile_default: Name = Name(String::from("dexdec")), "decompile.default", "ASC_DECOMPILER", None,
506 "backend of asc decompile unless --decompiler names one (a [decompilers.NAME] table, or the built-in dexdec)";
507 decompile_fallback: Names = Names::default(), "decompile.fallback", "ASC_DECOMPILE_FALLBACK", None,
508 "backends tried in order when the default one fails on a whole class; none: smali right away";
509 decompile_mapping: MaybePath = MaybePath::default(), "decompile.mapping", "ASC_MAPPING", None,
510 "R8 / ProGuard mapping.txt whose names decompiled source shows (--mapping); none: the DEX's own names";
511 decompile_auto_install: bool = true, "decompile.auto_install", "ASC_AUTO_INSTALL", None,
512 "download asc-dexdec-worker speaking this asc's protocol version from decompile.release_url when no worker is found";
513 decompile_release_url: Name = Name(String::from(DEFAULT_RELEASE_URL)), "decompile.release_url", "ASC_RELEASE_URL", None,
514 "where asc install and the auto-install fetch the worker: URL/asc-dexdec-worker-pPROTOCOL-TARGET.tar.gz";
515}
516
517pub const DEFAULT_RELEASE_URL: &str = "https://github.com/DroidASC/asc-dexdec/releases/latest/download";
520
521const LIST_KEYS: &[&str] = &["decompile.fallback"];
524
525fn flatten<'t>(prefix: &str, table: &'t toml::Table, out: &mut Vec<(String, &'t toml::Value)>) {
527 for (k, v) in table {
528 let name = if prefix.is_empty() { k.clone() } else { format!("{prefix}.{k}") };
529 match v {
530 toml::Value::Table(t) => flatten(&name, t, out),
531 v => out.push((name, v)),
532 }
533 }
534}
535
536fn parse_value<T: Value>(key: &str, text: &str, origin: &str) -> Result<T, ConfigError> {
537 T::parse(text).ok_or_else(|| ConfigError::Invalid { origin: origin.into(), key: key.into(), text: text.into(), expected: T::EXPECTED })
538}
539
540impl Config {
541 fn known_keys() -> String {
542 Config::KEYS.iter().map(|k| k.name).collect::<Vec<_>>().join(", ")
543 }
544
545 pub fn file_path(env: impl Fn(&str) -> Option<String>) -> Option<PathBuf> {
549 if let Some(p) = env(CONFIG_ENV) {
550 return (!p.is_empty()).then(|| expand_home(&p));
551 }
552 let set = |name: &str| env(name).filter(|d| !d.is_empty()).map(PathBuf::from);
553 let base = set("XDG_CONFIG_HOME")
554 .or_else(|| if cfg!(windows) { set("APPDATA") } else { None })
555 .or_else(|| set("HOME").map(|h| h.join(".config")))?;
556 Some(base.join("asc").join("config.toml"))
557 }
558
559 pub fn load(flags: &[(String, String)]) -> Result<Config, ConfigError> {
567 let env = |name: &str| std::env::var(name).ok();
568 let file = match Config::file_path(env) {
569 Some(path) => match std::fs::read_to_string(&path) {
570 Ok(text) => Some((path, text)),
571 Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
572 Err(e) => return Err(ConfigError::File { path: path.display().to_string(), message: e.to_string() }),
573 },
574 None => None,
575 };
576 Config::resolve(file.as_ref().map(|(p, t)| (p.as_path(), t.as_str())), env, flags)
577 }
578
579 pub fn resolve(
587 file: Option<(&Path, &str)>,
588 env: impl Fn(&str) -> Option<String>,
589 flags: &[(String, String)],
590 ) -> Result<Config, ConfigError> {
591 let mut config = Config::defaults();
592 if let Some((path, text)) = file {
593 config.apply_file(path, text)?;
594 config.file = Some(path.to_path_buf());
595 }
596 for key in Config::KEYS {
597 let found =
598 [Some(key.env), key.legacy_env].into_iter().flatten().find_map(|var| Some((var, env(var).filter(|v| !v.is_empty())?)));
599 if let Some((var, text)) = found {
600 config.set(key.name, &text, Source::Env, &format!("${var}"))?;
601 }
602 }
603 for (name, text) in flags {
604 config.set(name, text, Source::Flag, "command line")?;
605 }
606 Ok(config)
607 }
608
609 fn apply_file(&mut self, path: &Path, text: &str) -> Result<(), ConfigError> {
610 let origin = path.display().to_string();
611 let mut table: toml::Table =
612 text.parse().map_err(|e: toml::de::Error| ConfigError::File { path: origin.clone(), message: e.message().to_string() })?;
613 if let Some(decompilers) = table.remove("decompilers") {
615 self.decompilers = Decompiler::parse_all(&decompilers, &origin)?;
616 }
617 let mut pairs = Vec::new();
618 flatten("", &table, &mut pairs);
619 for (name, value) in pairs {
620 let text = match value {
621 toml::Value::String(s) => s.clone(),
622 toml::Value::Integer(i) => i.to_string(),
623 toml::Value::Boolean(b) => b.to_string(),
624 toml::Value::Array(items) if LIST_KEYS.contains(&name.as_str()) => {
625 let names: Option<Vec<&str>> = items.iter().map(toml::Value::as_str).collect();
626 match names {
627 Some(names) if names.is_empty() => "none".to_string(),
628 Some(names) => names.join(","),
629 None => {
630 return Err(ConfigError::Invalid {
631 origin,
632 key: name,
633 text: "<array>".into(),
634 expected: "an array of strings",
635 });
636 }
637 }
638 }
639 other => {
640 return Err(ConfigError::Invalid {
641 origin,
642 key: name,
643 text: format!("<{}>", other.type_str()),
644 expected: "a string, number or boolean",
645 });
646 }
647 };
648 self.set(&name, &text, Source::File, &origin)?;
649 }
650 Ok(())
651 }
652
653 pub fn flag_pairs(&self) -> Vec<(String, String)> {
656 self.entries()
657 .into_iter()
658 .filter(|(_, _, source, _)| *source == Source::Flag)
659 .map(|(key, text, _, _)| (key.name.to_string(), text))
660 .collect()
661 }
662
663 pub fn threads(&self) -> Threads {
665 match self.threads.value {
666 Count::Auto => Threads::Auto,
667 Count::Fixed(n) => Threads::Fixed(n),
668 }
669 }
670
671 pub fn fixed_threads(&self) -> Option<usize> {
673 match self.threads.value {
674 Count::Auto => None,
675 Count::Fixed(n) => Some(n),
676 }
677 }
678
679 pub fn memory_policy(&self, device: &Device) -> Option<MemoryPolicy> {
681 let percent = match self.memory_max.value {
682 Amount::Bytes(_) => return None,
683 Amount::Auto => MemoryPolicy::PERCENT,
684 Amount::Percent(p) => p,
685 };
686 Some(MemoryPolicy { percent, ceiling: self.memory_ceiling(device), split: self.memory_split.value })
687 }
688
689 fn memory_ceiling(&self, device: &Device) -> usize {
690 match self.memory_ceiling.value {
691 Amount::Auto => MemoryPolicy::auto_ceiling(device.total_memory),
692 Amount::Bytes(b) => usize::try_from(b).unwrap_or(usize::MAX),
693 Amount::Percent(p) => device
694 .total_memory
695 .map_or(MemoryPolicy::CEILING_MIN, |t| usize::try_from(u128::from(t) * u128::from(p) / 100).unwrap_or(usize::MAX)),
696 }
697 }
698
699 pub fn memory_bytes(&self, device: &Device) -> usize {
701 match self.memory_policy(device) {
702 Some(policy) => policy.alone(device.available_memory),
703 None => match self.memory_max.value {
704 Amount::Bytes(b) => usize::try_from(b).unwrap_or(usize::MAX),
705 Amount::Auto | Amount::Percent(_) => MemoryPolicy::CEILING_MIN,
706 },
707 }
708 }
709
710 pub fn cache_dir(&self) -> Option<PathBuf> {
713 if !self.cache_enabled.value {
714 return None;
715 }
716 match &self.cache_dir.value {
717 CacheDir::Default => DiskCache::default_dir(),
718 CacheDir::Path(p) => Some(p.clone()),
719 }
720 }
721
722 pub fn disk_capacity(&self) -> DiskCapacity {
724 let ceiling = self.cache_ceiling.value.0;
725 match self.cache_max.value {
726 Amount::Auto => DiskCapacity::Auto(AutoDisk::new(AutoDisk::PERCENT, ceiling)),
727 Amount::Percent(percent) => DiskCapacity::Auto(AutoDisk::new(percent, ceiling)),
728 Amount::Bytes(b) => DiskCapacity::Fixed(b),
729 }
730 }
731
732 pub fn disk_cache(&self) -> Option<DiskCache> {
734 let tiers: Vec<Tier> = Tier::ALL.into_iter().filter(|t| self.cache_tiers.value.0[*t as usize]).collect();
735 Some(
736 DiskCache::open(self.cache_dir()?)
737 .with_capacity_mode(self.disk_capacity())
738 .with_tiers(&tiers)
739 .with_min_dex_bytes(self.cache_min_dex.value.0),
740 )
741 }
742
743 fn writers(&self) -> Option<usize> {
744 match self.cache_writers.value {
745 Count::Auto => None,
746 Count::Fixed(n) => Some(n),
747 }
748 }
749
750 pub fn one_shot_cache(&self) -> CacheConfig {
752 CacheConfig {
753 disk: self.disk_cache(),
754 writers: self.writers(),
755 write_queue_bytes: self.write_queue_bytes(),
756 ..CacheConfig::default()
757 }
758 }
759
760 pub fn resident_cache(&self, device: &Device) -> CacheConfig {
762 CacheConfig {
763 memory_bytes: self.memory_bytes(device),
764 disk: self.disk_cache(),
765 memory_auto: self.memory_policy(device),
766 write_queue_bytes: self.write_queue_bytes(),
767 writers: self.writers(),
768 }
769 }
770
771 fn write_queue_bytes(&self) -> usize {
772 usize::try_from(self.cache_write_queue.value.0).unwrap_or(usize::MAX)
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use std::collections::HashMap;
780
781 const GIB: u64 = 1 << 30;
782
783 fn env(vars: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> + use<> {
784 let vars: HashMap<String, String> = vars.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect();
785 move |k| vars.get(k).cloned()
786 }
787
788 fn flags(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
789 pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
790 }
791
792 #[test]
793 fn values_parse_and_show_the_same_text() {
794 assert_eq!(Amount::parse("512"), Some(Amount::Bytes(512 << 20)));
795 assert_eq!(Amount::parse(" 4G "), Some(Amount::Bytes(4 * GIB)));
796 assert_eq!(Amount::parse("4GiB"), Some(Amount::Bytes(4 * GIB)));
797 assert_eq!(Amount::parse("1024k"), Some(Amount::Bytes(1 << 20)));
798 assert_eq!(Amount::parse("25%"), Some(Amount::Percent(25)));
799 assert_eq!(Amount::parse("0%"), None);
800 assert_eq!(Amount::parse("lots"), None);
801 assert_eq!(Amount::Bytes(6 * GIB).to_string(), "6G");
802 assert_eq!(Amount::Bytes(1536 << 20).to_string(), "1536M");
803 assert_eq!(Bytes(0).to_string(), "0B");
804 assert_eq!(Count::parse("-1"), None);
805 assert_eq!(Count::parse("8"), Some(Count::Fixed(8)));
806 assert_eq!(Tiers::parse("dex, rendered"), Some(Tiers([true, false, true])));
807 assert_eq!(Tiers::parse("l1"), None);
808 assert_eq!(Tiers([false; 3]).to_string(), "none");
809 assert_eq!(Seconds::parse("15m").map(|s| s.0), Some(Duration::from_secs(900)));
810 assert_eq!(Seconds::parse("2").map(|s| s.to_string()).as_deref(), Some("2s"));
811 assert_eq!(Millis::parse("300").map(|s| s.to_string()).as_deref(), Some("300ms"));
812 assert_eq!(bool::parse("OFF"), Some(false));
813 for (key, text, _, _) in Config::defaults().entries() {
814 assert!(Config::defaults().set(key.name, &text, Source::Flag, "test").is_ok(), "{} = {text} does not read back", key.name);
815 }
816 }
817
818 #[test]
819 fn flags_beat_env_beat_file_beat_defaults() {
820 let file = "threads = 3\n[memory]\nmax = \"30%\"\nsplit = false\n[cache]\nmax = \"8G\"\ntiers = \"dex\"\n";
821 let path = Path::new("/etc/asc.toml");
822 let c =
823 Config::resolve(Some((path, file)), env(&[("ASC_THREADS", "6"), ("ASC_CACHE_MAX_MB", "1024")]), &flags(&[("threads", "9")]))
824 .unwrap();
825 assert_eq!(c.threads, Setting { value: Count::Fixed(9), source: Source::Flag });
826 assert_eq!(c.cache_max, Setting { value: Amount::Bytes(GIB), source: Source::Env });
827 assert_eq!(c.memory_max, Setting { value: Amount::Percent(30), source: Source::File });
828 assert!(!c.memory_split.value);
829 assert_eq!(c.cache_tiers.value, Tiers([true, false, false]));
830 assert_eq!(c.cache_dir.source, Source::Default);
831 assert_eq!(c.file.as_deref(), Some(path));
832 assert_eq!(c.flag_pairs(), flags(&[("threads", "9")]));
833 let c = Config::resolve(None, env(&[("ASC_CACHE_MAX", "20%"), ("ASC_CACHE_MAX_MB", "1"), ("ASC_THREADS", "")]), &[]).unwrap();
835 assert_eq!(c.cache_max.value, Amount::Percent(20));
836 assert_eq!(c.threads.source, Source::Default);
837 }
838
839 #[test]
840 fn errors_say_where_and_what_is_expected() {
841 let e = Config::resolve(None, env(&[("ASC_MEMORY", "lots")]), &[]).unwrap_err().to_string();
842 assert!(e.contains("$ASC_MEMORY: memory.max = \"lots\": expected auto, a size"), "{e}");
843 let e = Config::resolve(Some((Path::new("c.toml"), "[memory]\nmaxx = 1\n")), env(&[]), &[]).unwrap_err().to_string();
844 assert!(e.starts_with("c.toml: unknown setting \"memory.maxx\"; known: threads, memory.max"), "{e}");
845 let e = Config::resolve(Some((Path::new("c.toml"), "threads = [1]\n")), env(&[]), &[]).unwrap_err().to_string();
846 assert!(e.contains("expected a string, number or boolean"), "{e}");
847 let e = Config::resolve(Some((Path::new("c.toml"), "threads = \n")), env(&[]), &[]).unwrap_err().to_string();
848 assert!(e.starts_with("c.toml: "), "{e}");
849 let e = Config::resolve(None, env(&[]), &flags(&[("cache.tiers", "l1")])).unwrap_err().to_string();
850 assert!(e.contains("command line: cache.tiers = \"l1\": expected a list of dex, derived, rendered"), "{e}");
851 }
852
853 #[test]
854 fn the_file_is_found_like_other_xdg_configs() {
855 assert_eq!(Config::file_path(env(&[("HOME", "/h")])), Some(PathBuf::from("/h/.config/asc/config.toml")));
856 assert_eq!(Config::file_path(env(&[("HOME", "/h"), ("XDG_CONFIG_HOME", "/x")])), Some(PathBuf::from("/x/asc/config.toml")));
857 assert_eq!(Config::file_path(env(&[("HOME", "/h"), (CONFIG_ENV, "/c.toml")])), Some(PathBuf::from("/c.toml")));
858 assert_eq!(Config::file_path(env(&[("HOME", "/h"), (CONFIG_ENV, "")])), None);
859 }
860
861 #[test]
862 fn auto_values_follow_the_device() {
863 let laptop = Device { total_memory: Some(12 * GIB), available_memory: Some(6 * GIB), cpus: 16 };
864 let big = Device { total_memory: Some(96 * GIB), available_memory: Some(60 * GIB), cpus: 32 };
865 let c = Config::defaults();
866 assert_eq!(c.memory_bytes(&laptop), (6 * GIB / 4) as usize);
867 assert_eq!(c.memory_bytes(&big), 6 << 30);
868 assert_eq!(c.memory_policy(&big), Some(MemoryPolicy { percent: 25, ceiling: 6 << 30, split: true }));
869 let c = Config::resolve(None, env(&[]), &flags(&[("memory.max", "10%"), ("memory.ceiling", "50%")])).unwrap();
870 assert_eq!(c.memory_bytes(&big), (60 * GIB / 10) as usize);
871 assert_eq!(c.memory_policy(&big).unwrap().ceiling, (48 * GIB) as usize);
872 let c = Config::resolve(None, env(&[]), &flags(&[("memory.max", "1G")])).unwrap();
873 assert_eq!((c.memory_bytes(&big), c.memory_policy(&big)), (GIB as usize, None));
874 let c = Config::resolve(None, env(&[]), &flags(&[("cache.max", "10%"), ("cache.ceiling", "5G")])).unwrap();
875 assert_eq!(c.disk_capacity(), DiskCapacity::Auto(AutoDisk::new(10, 5 * GIB)));
876 let off = Config::resolve(None, env(&[("ASC_CACHE", "off")]), &[]).unwrap();
877 assert!(off.disk_cache().is_none() && off.cache_dir().is_none());
878 let fixed = Config::resolve(None, env(&[]), &flags(&[("threads", "4"), ("cache.writers", "2"), ("cache.dir", "/tmp/c")])).unwrap();
879 assert_eq!((fixed.threads(), fixed.fixed_threads()), (Threads::Fixed(4), Some(4)));
880 let cache = fixed.resident_cache(&laptop);
881 assert_eq!((cache.writers, cache.write_queue_bytes), (Some(2), CacheConfig::WRITE_QUEUE));
882 assert_eq!(cache.disk.unwrap().dir(), Path::new("/tmp/c"));
883 }
884}