1use std::{fmt, str::FromStr};
9
10use crate::{DEFAULT_VALUE, FieldError};
11#[cfg(doc)]
12use crate::{ENV_VAR, Rule};
13
14fn without_nul(text: String) -> Result<String, FieldError> {
16 if text.contains('\0') {
17 Err(FieldError::Nul)
18 } else {
19 Ok(text)
20 }
21}
22
23const STEAM_PREFIX: &str = "steam:";
26
27fn after_steam_prefix(text: &str) -> Option<&str> {
30 let (word, rest) = text.split_at_checked(STEAM_PREFIX.len())?;
31 word.eq_ignore_ascii_case(STEAM_PREFIX).then_some(rest)
32}
33
34pub fn steam_app_id(digits: &str) -> Option<u32> {
37 if !digits.bytes().all(|byte| byte.is_ascii_digit()) {
38 return None;
39 }
40 digits.parse().ok().filter(|&id| id != 0)
41}
42
43#[derive(Clone, Debug, PartialEq, Eq, Hash)]
47pub enum Exe {
48 Any,
50 Name(ExeName),
52 Steam(SteamApp),
55}
56
57#[derive(Clone, Debug, PartialEq, Eq, Hash)]
61pub struct ExeName(String);
62
63#[derive(Clone, Debug, PartialEq, Eq, Hash)]
66pub struct SteamApp {
67 id: u32,
69 text: String,
71}
72
73impl Exe {
74 pub const ANY: &'static str = "*";
76
77 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
88 let text = without_nul(text.into())?;
89 if let Some(digits) = after_steam_prefix(&text) {
90 let app = steam_app_id(digits).map(SteamApp::of_id);
91 return app.map(Self::Steam).ok_or(FieldError::BadSteamApp(text));
92 }
93 match text.as_str() {
94 "" => Err(FieldError::MissingExe),
95 Self::ANY => Ok(Self::Any),
96 _ => Ok(Self::Name(ExeName(text))),
97 }
98 }
99
100 pub(crate) fn literal(text: &str) -> Self {
104 if text == Self::ANY {
105 return Self::Any;
106 }
107 after_steam_prefix(text).and_then(steam_app_id).map_or_else(
108 || Self::Name(ExeName(text.to_owned())),
109 |id| Self::Steam(SteamApp::of_id(id)),
110 )
111 }
112
113 #[must_use]
115 pub fn as_str(&self) -> &str {
116 match self {
117 Self::Any => Self::ANY,
118 Self::Name(name) => name.as_str(),
119 Self::Steam(app) => app.as_str(),
120 }
121 }
122
123 #[must_use]
125 pub const fn is_any(&self) -> bool {
126 matches!(self, Self::Any)
127 }
128
129 #[must_use]
131 pub const fn is_steam_app(&self) -> bool {
132 matches!(self, Self::Steam(_))
133 }
134
135 #[must_use]
138 pub fn matches(&self, basename: &str, steam_app: Option<u32>) -> bool {
139 match self {
140 Self::Any => true,
141 Self::Name(name) => basename.eq_ignore_ascii_case(name.as_str()),
142 Self::Steam(app) => steam_app == Some(app.id()),
143 }
144 }
145}
146
147impl ExeName {
148 #[must_use]
150 pub fn as_str(&self) -> &str {
151 &self.0
152 }
153}
154
155impl SteamApp {
156 fn of_id(id: u32) -> Self {
158 Self {
159 id,
160 text: format!("{STEAM_PREFIX}{id}"),
161 }
162 }
163
164 #[must_use]
166 pub const fn id(&self) -> u32 {
167 self.id
168 }
169
170 #[must_use]
172 pub fn as_str(&self) -> &str {
173 &self.text
174 }
175}
176
177impl FromStr for Exe {
178 type Err = FieldError;
179
180 fn from_str(s: &str) -> Result<Self, Self::Err> {
182 Self::new(s)
183 }
184}
185
186impl fmt::Display for Exe {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(self.as_str())
189 }
190}
191
192impl fmt::Display for ExeName {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.write_str(self.as_str())
195 }
196}
197
198impl fmt::Display for SteamApp {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 f.write_str(self.as_str())
201 }
202}
203
204impl PartialEq<&str> for Exe {
205 fn eq(&self, other: &&str) -> bool {
206 self.as_str() == *other
207 }
208}
209
210#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
214pub enum Specificity {
215 Any,
217 AnyWithFingerprint,
219 Exe,
221 ExeWithFingerprint,
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, Hash)]
229pub struct Fingerprint(String);
230
231impl Fingerprint {
232 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
238 without_nul(text.into()).map(Self)
239 }
240
241 pub(crate) fn literal(text: &str) -> Self {
244 Self(text.to_owned())
245 }
246
247 #[must_use]
249 pub fn as_str(&self) -> &str {
250 &self.0
251 }
252
253 #[must_use]
256 pub fn matches(&self, actual: &str) -> bool {
257 actual
258 .to_ascii_lowercase()
259 .contains(&self.0.to_ascii_lowercase())
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq, Hash)]
265pub struct Argument(String);
266
267impl Argument {
268 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
275 let text = without_nul(text.into())?;
276 if text.is_empty() {
277 Err(FieldError::EmptyArgument)
278 } else {
279 Ok(Self(text))
280 }
281 }
282
283 pub(crate) fn literal(text: &str) -> Self {
286 Self(text.to_owned())
287 }
288
289 #[must_use]
291 pub fn as_str(&self) -> &str {
292 &self.0
293 }
294}
295
296#[derive(Clone, Debug, PartialEq, Eq, Hash)]
304pub struct DebugChannel(String);
305
306impl DebugChannel {
307 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
315 let text = without_nul(text.into())?;
316 if text.is_empty() {
317 Err(FieldError::EmptyDebugChannel)
318 } else if text.contains(',') {
319 Err(FieldError::CommaInDebugChannel(text))
320 } else {
321 Ok(Self(text))
322 }
323 }
324
325 #[must_use]
327 pub fn as_str(&self) -> &str {
328 &self.0
329 }
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Hash)]
338pub struct DllOverride(String);
339
340impl DllOverride {
341 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
348 let text = without_nul(text.into())?;
349 if text.contains('=') {
350 Ok(Self(text))
351 } else {
352 Err(FieldError::InvalidDllOverride(text))
353 }
354 }
355
356 pub(crate) fn literal(text: &str) -> Self {
360 Self(text.to_owned())
361 }
362
363 #[must_use]
365 pub fn as_str(&self) -> &str {
366 &self.0
367 }
368
369 pub fn modules(&self) -> impl Iterator<Item = &str> {
371 self.names()
372 .split(',')
373 .map(str::trim)
374 .filter(|name| !name.is_empty())
375 }
376
377 #[must_use]
379 pub fn order(&self) -> &str {
380 self.0.split_once('=').map_or("", |(_, order)| order)
381 }
382
383 #[must_use]
387 pub fn is_default(&self) -> bool {
388 self.order().trim().eq_ignore_ascii_case(DEFAULT_VALUE)
389 }
390
391 fn names(&self) -> &str {
393 self.0.split_once('=').map_or("", |(names, _)| names)
394 }
395}
396
397#[derive(Clone, Debug, PartialEq, Eq, Hash)]
401pub struct EnvName(String);
402
403impl EnvName {
404 pub fn new(text: impl Into<String>) -> Result<Self, FieldError> {
411 let text = without_nul(text.into())?;
412 if text.is_empty() || text.contains('=') {
413 Err(FieldError::InvalidEnvName(text))
414 } else {
415 Ok(Self(text))
416 }
417 }
418
419 #[must_use]
421 pub fn as_str(&self) -> &str {
422 &self.0
423 }
424}
425
426#[derive(Clone, Debug, PartialEq, Eq, Hash)]
429pub struct EnvEntry {
430 name: EnvName,
432 value: Option<String>,
434}
435
436impl EnvEntry {
437 pub fn new(name: EnvName, value: impl Into<String>) -> Result<Self, FieldError> {
444 let value = without_nul(value.into())?;
445 Ok(Self {
446 name,
447 value: (!value.is_empty()).then_some(value),
448 })
449 }
450
451 #[must_use]
453 pub const fn remove(name: EnvName) -> Self {
454 Self { name, value: None }
455 }
456
457 pub(crate) fn literal(name: &str, value: &str) -> Self {
461 Self {
462 name: EnvName(name.to_owned()),
463 value: (!value.is_empty()).then(|| value.to_owned()),
464 }
465 }
466
467 #[must_use]
469 pub const fn name(&self) -> &EnvName {
470 &self.name
471 }
472
473 #[must_use]
475 pub fn value(&self) -> Option<&str> {
476 self.value.as_deref()
477 }
478}
479
480impl fmt::Display for EnvEntry {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483 write!(f, "{}={}", self.name, self.value().unwrap_or_default())
484 }
485}
486
487impl FromStr for Fingerprint {
488 type Err = FieldError;
489
490 fn from_str(s: &str) -> Result<Self, Self::Err> {
492 Self::new(s)
493 }
494}
495
496impl FromStr for Argument {
497 type Err = FieldError;
498
499 fn from_str(s: &str) -> Result<Self, Self::Err> {
501 Self::new(s)
502 }
503}
504
505impl FromStr for DebugChannel {
506 type Err = FieldError;
507
508 fn from_str(s: &str) -> Result<Self, Self::Err> {
510 Self::new(s)
511 }
512}
513
514impl FromStr for DllOverride {
515 type Err = FieldError;
516
517 fn from_str(s: &str) -> Result<Self, Self::Err> {
519 Self::new(s)
520 }
521}
522
523impl FromStr for EnvName {
524 type Err = FieldError;
525
526 fn from_str(s: &str) -> Result<Self, Self::Err> {
528 Self::new(s)
529 }
530}
531
532impl fmt::Display for Fingerprint {
533 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534 f.write_str(self.as_str())
535 }
536}
537
538impl fmt::Display for Argument {
539 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540 f.write_str(self.as_str())
541 }
542}
543
544impl fmt::Display for DebugChannel {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546 f.write_str(self.as_str())
547 }
548}
549
550impl fmt::Display for DllOverride {
551 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552 f.write_str(self.as_str())
553 }
554}
555
556impl fmt::Display for EnvName {
557 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
558 f.write_str(self.as_str())
559 }
560}
561
562impl AsRef<str> for Argument {
563 fn as_ref(&self) -> &str {
564 self.as_str()
565 }
566}
567
568impl PartialEq<&str> for Fingerprint {
569 fn eq(&self, other: &&str) -> bool {
570 self.0 == *other
571 }
572}
573
574impl PartialEq<&str> for Argument {
575 fn eq(&self, other: &&str) -> bool {
576 self.0 == *other
577 }
578}
579
580impl PartialEq<&str> for DebugChannel {
581 fn eq(&self, other: &&str) -> bool {
582 self.0 == *other
583 }
584}
585
586impl PartialEq<&str> for DllOverride {
587 fn eq(&self, other: &&str) -> bool {
588 self.0 == *other
589 }
590}
591
592impl PartialEq<&str> for EnvName {
593 fn eq(&self, other: &&str) -> bool {
594 self.0 == *other
595 }
596}
597
598#[cfg(test)]
599mod tests;