1use std::cmp::Ordering;
2use std::collections::{BTreeMap, BTreeSet, HashSet};
3use std::fmt::{Display, Formatter};
4use std::fs;
5use std::io::{Cursor, Read};
6#[cfg(windows)]
7use std::mem::{MaybeUninit, size_of};
8#[cfg(windows)]
9use std::os::windows::ffi::OsStringExt;
10use std::path::{Path, PathBuf};
11use std::sync::RwLock;
12
13use anyhow::{Context, Result, anyhow, bail};
14use chrono::{DateTime, Duration, Utc};
15use reqwest::blocking::{Client, Response};
16use rusqlite::types::{Value as SqlValue, ValueRef};
17use rusqlite::{Connection, OpenFlags, Row as SqlRow, params_from_iter};
18use serde::{Deserialize, Serialize};
19use serde_json::{Map as JsonMap, Value as JsonValue};
20use serde_yaml::{Mapping as YamlMapping, Value as YamlValue};
21use sha2::{Digest, Sha256};
22#[cfg(windows)]
23use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, LocalFree};
24#[cfg(windows)]
25use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW;
26#[cfg(windows)]
27use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser};
28#[cfg(windows)]
29use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
30#[cfg(windows)]
31use winreg::{RegKey, enums::*};
32use zip::ZipArchive;
33
34const DEFAULT_MARKET: &str = "US";
35const DEFAULT_MAX_RESULTS: usize = 50;
36const LIST_LOOKUP_MAX_RESULTS: usize = 500;
37const PREINDEXED_CANDIDATES: &[&str] = &["source2.msix", "source.msix"];
38const DEFAULT_USER_AGENT: &str = "pinget-rs/0.1";
39#[cfg(windows)]
40const PACKAGED_FAMILY_NAME: &str = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe";
41#[cfg(windows)]
42const PACKAGED_NAME: &str = "Microsoft.DesktopAppInstaller";
43const INSTALLED_STATE_UNSUPPORTED_WARNING: &str =
44 "Installed package discovery is not supported on this platform; returning no installed packages.";
45const INSTALL_UNSUPPORTED_WARNING: &str =
46 "Installing packages is not supported on this platform; no changes were made.";
47const REPAIR_UNSUPPORTED_WARNING: &str = "Repairing packages is not supported on this platform; no changes were made.";
48const REPAIR_REINSTALL_WARNING: &str =
49 "Pinget repair currently re-runs the package install flow for the selected package.";
50const UNINSTALL_UNSUPPORTED_WARNING: &str =
51 "Uninstalling packages is not supported on this platform; no changes were made.";
52#[cfg(windows)]
53const WINGET_PACKAGE_NOT_FOUND_EXIT_CODE: i32 = -1978335212;
54const SUPPORTED_ADMIN_SETTINGS: &[&str] = &[
55 "LocalManifestFiles",
56 "BypassCertificatePinningForMicrosoftStore",
57 "InstallerHashOverride",
58 "LocalArchiveMalwareScanOverride",
59 "ProxyCommandLineOptions",
60];
61const REST_SUPPORTED_CONTRACTS: &[&str] = &[
62 "1.12.0", "1.10.0", "1.9.0", "1.7.0", "1.6.0", "1.5.0", "1.4.0", "1.1.0", "1.0.0",
63];
64
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
66pub enum SourceKind {
67 #[serde(rename = "preIndexed", alias = "PreIndexed")]
68 PreIndexed,
69 #[serde(rename = "rest", alias = "Rest")]
70 Rest,
71}
72
73impl Display for SourceKind {
74 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75 match self {
76 SourceKind::PreIndexed => f.write_str("preindexed"),
77 SourceKind::Rest => f.write_str("rest"),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct SourceRecord {
84 pub name: String,
85 pub kind: SourceKind,
86 pub arg: String,
87 pub identifier: String,
88 #[serde(default = "default_source_trust_level")]
89 pub trust_level: String,
90 #[serde(default)]
91 pub explicit: bool,
92 #[serde(default)]
93 pub priority: i32,
94 #[serde(default)]
95 pub last_update: Option<DateTime<Utc>>,
96 #[serde(default)]
97 pub source_version: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
101struct SourceStore {
102 sources: Vec<SourceRecord>,
103}
104
105impl Default for SourceStore {
106 fn default() -> Self {
107 Self {
108 sources: vec![
109 SourceRecord {
110 name: "winget".to_owned(),
111 kind: SourceKind::PreIndexed,
112 arg: "https://cdn.winget.microsoft.com/cache".to_owned(),
113 identifier: "Microsoft.Winget.Source_8wekyb3d8bbwe".to_owned(),
114 trust_level: "Trusted".to_owned(),
115 explicit: false,
116 priority: 0,
117 last_update: None,
118 source_version: None,
119 },
120 SourceRecord {
121 name: "msstore".to_owned(),
122 kind: SourceKind::Rest,
123 arg: "https://storeedgefd.dsx.mp.microsoft.com/v9.0".to_owned(),
124 identifier: "StoreEdgeFD".to_owned(),
125 trust_level: "Trusted".to_owned(),
126 explicit: false,
127 priority: 0,
128 last_update: None,
129 source_version: None,
130 },
131 ],
132 }
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct RepositoryOptions {
139 pub app_root: PathBuf,
140 pub user_agent: String,
141}
142
143impl RepositoryOptions {
144 pub fn new(app_root: impl Into<PathBuf>) -> Self {
146 Self {
147 app_root: app_root.into(),
148 user_agent: DEFAULT_USER_AGENT.to_owned(),
149 }
150 }
151
152 pub fn for_current_user() -> Result<Self> {
154 Ok(Self::new(default_app_root()?))
155 }
156
157 #[must_use]
159 pub fn with_user_agent(mut self, user_agent: impl Into<String>) -> Self {
160 self.user_agent = user_agent.into();
161 self
162 }
163}
164
165#[derive(Debug, Clone, Default)]
166pub struct PackageQuery {
167 pub query: Option<String>,
168 pub id: Option<String>,
169 pub name: Option<String>,
170 pub moniker: Option<String>,
171 pub tag: Option<String>,
172 pub command: Option<String>,
173 pub source: Option<String>,
174 pub count: Option<usize>,
175 pub exact: bool,
176 pub version: Option<String>,
177 pub channel: Option<String>,
178 pub locale: Option<String>,
179 pub installer_type: Option<String>,
180 pub installer_architecture: Option<String>,
181 pub platform: Option<String>,
182 pub os_version: Option<String>,
183 pub install_scope: Option<String>,
184}
185
186#[derive(Debug, Clone, Default)]
187pub struct ListQuery {
188 pub query: Option<String>,
189 pub id: Option<String>,
190 pub name: Option<String>,
191 pub moniker: Option<String>,
192 pub tag: Option<String>,
193 pub command: Option<String>,
194 pub product_code: Option<String>,
195 pub version: Option<String>,
196 pub source: Option<String>,
197 pub count: Option<usize>,
198 pub exact: bool,
199 pub install_scope: Option<String>,
200 pub upgrade_only: bool,
201 pub include_unknown: bool,
202 pub include_pinned: bool,
203}
204
205#[derive(Debug, Clone, serde::Serialize)]
206pub struct SearchMatch {
207 pub source_name: String,
208 pub source_kind: SourceKind,
209 pub id: String,
210 pub name: String,
211 pub moniker: Option<String>,
212 pub version: Option<String>,
213 pub channel: Option<String>,
214 pub match_criteria: Option<String>,
215}
216
217#[derive(Debug, Clone, serde::Serialize)]
218pub struct SearchResponse {
219 pub matches: Vec<SearchMatch>,
220 pub warnings: Vec<String>,
221 pub truncated: bool,
222}
223
224#[derive(Debug, Clone, serde::Serialize)]
225pub struct ListMatch {
226 pub name: String,
227 pub id: String,
228 pub local_id: String,
229 pub installed_version: String,
230 pub available_version: Option<String>,
231 pub source_name: Option<String>,
232 pub publisher: Option<String>,
233 pub scope: Option<String>,
234 pub installer_category: Option<String>,
235 pub install_location: Option<String>,
236 pub package_family_names: Vec<String>,
237 pub product_codes: Vec<String>,
238 pub upgrade_codes: Vec<String>,
239}
240
241#[derive(Debug, Clone, serde::Serialize)]
242pub struct ListResponse {
243 pub matches: Vec<ListMatch>,
244 pub warnings: Vec<String>,
245 pub truncated: bool,
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
249pub struct VersionKey {
250 pub version: String,
251 pub channel: String,
252}
253
254#[derive(Debug, Clone, serde::Serialize)]
255pub struct Manifest {
256 pub id: String,
257 pub name: String,
258 pub version: String,
259 pub channel: String,
260 pub publisher: Option<String>,
261 pub description: Option<String>,
262 pub moniker: Option<String>,
263 pub package_url: Option<String>,
264 pub publisher_url: Option<String>,
265 pub publisher_support_url: Option<String>,
266 pub license: Option<String>,
267 pub license_url: Option<String>,
268 pub privacy_url: Option<String>,
269 pub author: Option<String>,
270 pub copyright: Option<String>,
271 pub copyright_url: Option<String>,
272 pub release_notes: Option<String>,
273 pub release_notes_url: Option<String>,
274 pub tags: Vec<String>,
275 pub agreements: Vec<PackageAgreement>,
276 pub package_dependencies: Vec<String>,
277 pub documentation: Vec<Documentation>,
278 pub installers: Vec<Installer>,
279}
280
281#[derive(Debug, Clone, serde::Serialize)]
282pub struct Documentation {
283 pub label: Option<String>,
284 pub url: String,
285}
286
287#[derive(Debug, Clone, serde::Serialize)]
288pub struct PackageAgreement {
289 pub label: Option<String>,
290 pub text: Option<String>,
291 pub url: Option<String>,
292}
293
294#[derive(Debug, Clone, serde::Serialize)]
295pub struct Installer {
296 pub architecture: Option<String>,
297 pub installer_type: Option<String>,
298 pub url: Option<String>,
299 pub sha256: Option<String>,
300 pub product_code: Option<String>,
301 pub locale: Option<String>,
302 pub scope: Option<String>,
303 pub release_date: Option<String>,
304 pub package_family_name: Option<String>,
305 pub upgrade_code: Option<String>,
306 pub platforms: Vec<String>,
307 pub minimum_os_version: Option<String>,
308 pub switches: InstallerSwitches,
309 pub commands: Vec<String>,
310 pub package_dependencies: Vec<String>,
311}
312
313#[derive(Debug, Clone, Default, serde::Serialize)]
314pub struct InstallerSwitches {
315 pub silent: Option<String>,
316 pub silent_with_progress: Option<String>,
317 pub interactive: Option<String>,
318 pub custom: Option<String>,
319 pub log: Option<String>,
320 pub install_location: Option<String>,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub enum InstallerMode {
325 Interactive,
326 SilentWithProgress,
327 Silent,
328}
329
330#[derive(Debug, Clone)]
331pub struct InstallRequest {
332 pub query: PackageQuery,
333 pub manifest_path: Option<PathBuf>,
334 pub mode: InstallerMode,
335 pub log_path: Option<PathBuf>,
336 pub custom: Option<String>,
337 pub override_args: Option<String>,
338 pub install_location: Option<String>,
339 pub skip_dependencies: bool,
340 pub dependencies_only: bool,
341 pub accept_package_agreements: bool,
342 pub force: bool,
343 pub rename: Option<String>,
344 pub uninstall_previous: bool,
345 pub ignore_security_hash: bool,
346 pub dependency_source: Option<String>,
347 pub no_upgrade: bool,
348}
349
350impl InstallRequest {
351 pub fn new(query: PackageQuery) -> Self {
352 Self {
353 query,
354 manifest_path: None,
355 mode: InstallerMode::SilentWithProgress,
356 log_path: None,
357 custom: None,
358 override_args: None,
359 install_location: None,
360 skip_dependencies: false,
361 dependencies_only: false,
362 accept_package_agreements: false,
363 force: false,
364 rename: None,
365 uninstall_previous: false,
366 ignore_security_hash: false,
367 dependency_source: None,
368 no_upgrade: false,
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
374pub struct UninstallRequest {
375 pub query: PackageQuery,
376 pub manifest_path: Option<PathBuf>,
377 pub product_code: Option<String>,
378 pub mode: InstallerMode,
379 pub all_versions: bool,
380 pub force: bool,
381 pub purge: bool,
382 pub preserve: bool,
383 pub log_path: Option<PathBuf>,
384}
385
386impl UninstallRequest {
387 pub fn new(query: PackageQuery) -> Self {
388 Self {
389 query,
390 manifest_path: None,
391 product_code: None,
392 mode: InstallerMode::SilentWithProgress,
393 all_versions: false,
394 force: false,
395 purge: false,
396 preserve: false,
397 log_path: None,
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
403pub struct RepairRequest {
404 pub query: PackageQuery,
405 pub manifest_path: Option<PathBuf>,
406 pub product_code: Option<String>,
407 pub mode: InstallerMode,
408 pub log_path: Option<PathBuf>,
409 pub accept_package_agreements: bool,
410 pub force: bool,
411 pub ignore_security_hash: bool,
412}
413
414impl RepairRequest {
415 pub fn new(query: PackageQuery) -> Self {
416 Self {
417 query,
418 manifest_path: None,
419 product_code: None,
420 mode: InstallerMode::SilentWithProgress,
421 log_path: None,
422 accept_package_agreements: false,
423 force: false,
424 ignore_security_hash: false,
425 }
426 }
427}
428
429#[derive(Debug, Clone, serde::Serialize)]
430pub struct ShowResult {
431 pub package: SearchMatch,
432 pub manifest: Manifest,
433 pub selected_installer: Option<Installer>,
434 pub cached_files: Vec<PathBuf>,
435 pub warnings: Vec<String>,
436 #[serde(skip_serializing)]
437 manifest_documents: JsonValue,
438}
439
440impl ShowResult {
441 pub fn structured_document(&self) -> JsonValue {
442 collapse_structured_document(&self.manifest_documents)
443 }
444}
445
446#[derive(Debug, Clone, serde::Serialize)]
447pub struct VersionsResult {
448 pub package: SearchMatch,
449 pub versions: Vec<VersionKey>,
450 pub warnings: Vec<String>,
451}
452
453#[derive(Debug, Clone, serde::Serialize)]
454pub struct CacheWarmResult {
455 pub package: SearchMatch,
456 pub cached_files: Vec<PathBuf>,
457 pub warnings: Vec<String>,
458}
459
460#[derive(Debug, Clone, serde::Serialize)]
461pub struct SourceUpdateResult {
462 pub name: String,
463 pub kind: SourceKind,
464 pub detail: String,
465}
466
467#[derive(Debug, Clone, serde::Serialize)]
468pub struct PinRecord {
469 pub package_id: String,
470 pub version: String,
471 pub source_id: String,
472 pub pin_type: PinType,
473}
474
475#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
476pub enum PinType {
477 Pinning,
478 Blocking,
479 Gating,
480}
481
482impl Display for PinType {
483 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
484 match self {
485 PinType::Pinning => f.write_str("Pinning"),
486 PinType::Blocking => f.write_str("Blocking"),
487 PinType::Gating => f.write_str("Gating"),
488 }
489 }
490}
491
492#[derive(Debug, Clone, serde::Serialize)]
494pub struct InstallResult {
495 pub package_id: String,
496 pub version: String,
497 pub installer_path: PathBuf,
498 pub installer_type: String,
499 pub exit_code: i32,
500 pub success: bool,
501 pub no_op: bool,
502 pub warnings: Vec<String>,
503}
504
505impl InstallerSwitches {
506 fn with_fallback(&self, fallback: &Self) -> Self {
507 Self {
508 silent: self.silent.clone().or_else(|| fallback.silent.clone()),
509 silent_with_progress: self
510 .silent_with_progress
511 .clone()
512 .or_else(|| fallback.silent_with_progress.clone()),
513 interactive: self.interactive.clone().or_else(|| fallback.interactive.clone()),
514 custom: self.custom.clone().or_else(|| fallback.custom.clone()),
515 log: self.log.clone().or_else(|| fallback.log.clone()),
516 install_location: self
517 .install_location
518 .clone()
519 .or_else(|| fallback.install_location.clone()),
520 }
521 }
522}
523
524fn installed_package_discovery_supported() -> bool {
525 cfg!(windows)
526}
527
528fn package_actions_supported() -> bool {
529 cfg!(windows)
530}
531
532fn unsupported_action_result(
533 package_id: impl Into<String>,
534 version: impl Into<String>,
535 installer_type: impl Into<String>,
536 warning: &str,
537) -> InstallResult {
538 InstallResult {
539 package_id: package_id.into(),
540 version: version.into(),
541 installer_path: PathBuf::new(),
542 installer_type: installer_type.into(),
543 exit_code: 0,
544 success: true,
545 no_op: true,
546 warnings: vec![warning.to_owned()],
547 }
548}
549
550#[derive(Debug, Clone)]
551struct InstalledPackage {
552 name: String,
553 local_id: String,
554 installed_version: String,
555 publisher: Option<String>,
556 scope: Option<String>,
557 installer_category: Option<String>,
558 install_location: Option<String>,
559 package_family_names: Vec<String>,
560 product_codes: Vec<String>,
561 upgrade_codes: Vec<String>,
562 correlated: Option<SearchMatch>,
563}
564
565#[derive(Debug, Clone)]
566struct LocatedMatch {
567 display: SearchMatch,
568 source_index: usize,
569 locator: MatchLocator,
570}
571
572#[derive(Debug)]
573struct SearchSourceMatches {
574 matches: Vec<LocatedMatch>,
575 truncated: bool,
576}
577
578#[derive(Debug, Clone)]
579enum MatchLocator {
580 PreIndexedV1 {
581 package_rowid: i64,
582 },
583 PreIndexedV2 {
584 package_rowid: i64,
585 package_hash: String,
586 },
587 Rest {
588 package_id: String,
589 versions: Vec<VersionKey>,
590 },
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594enum SearchSemantics {
595 Many,
596 Single,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
600struct RestInfoCache {
601 expires_at: DateTime<Utc>,
602 value: RestInformation,
603}
604
605#[derive(Debug, Clone, Default, Serialize, Deserialize)]
606struct RestInformation {
607 #[serde(rename = "SourceIdentifier")]
608 source_identifier: String,
609 #[serde(rename = "ServerSupportedVersions", default)]
610 server_supported_versions: Vec<String>,
611 #[serde(rename = "RequiredPackageMatchFields", default)]
612 required_package_match_fields: Vec<String>,
613 #[serde(rename = "UnsupportedPackageMatchFields", default)]
614 unsupported_package_match_fields: Vec<String>,
615 #[serde(rename = "RequiredQueryParameters", default)]
616 required_query_parameters: Vec<String>,
617 #[serde(rename = "UnsupportedQueryParameters", default)]
618 unsupported_query_parameters: Vec<String>,
619}
620
621#[derive(Debug, Clone, Deserialize)]
622struct PackageVersionDataDocument {
623 #[serde(rename = "vD", default)]
624 versions: Vec<PackageVersionDataEntry>,
625}
626
627#[derive(Debug, Clone, Deserialize)]
628struct PackageVersionDataEntry {
629 #[serde(rename = "v")]
630 version: String,
631 #[serde(rename = "rP")]
632 manifest_relative_path: String,
633 #[serde(rename = "s256H")]
634 manifest_hash: String,
635}
636
637pub struct Repository {
638 app_root: PathBuf,
639 client: Client,
640 store: SourceStore,
641 use_system_winget_sources: bool,
642}
643
644impl Repository {
645 pub fn open() -> Result<Self> {
647 Self::open_with_options(RepositoryOptions::for_current_user()?)
648 }
649
650 pub fn open_with_options(options: RepositoryOptions) -> Result<Self> {
652 ensure_app_dirs(&options.app_root)?;
653 let use_system_winget_sources = uses_system_winget_source_commands(&options.app_root);
654 let store = load_store(&options.app_root)?;
655 let client = Client::builder()
656 .user_agent(&options.user_agent)
657 .build()
658 .context("failed to build HTTP client")?;
659 Ok(Self {
660 app_root: options.app_root,
661 client,
662 store,
663 use_system_winget_sources,
664 })
665 }
666
667 pub fn app_root(&self) -> &Path {
668 &self.app_root
669 }
670
671 pub fn list_sources(&self) -> Vec<SourceRecord> {
672 self.store.sources.clone()
673 }
674
675 pub fn add_source(&mut self, name: &str, arg: &str, kind: SourceKind) -> Result<()> {
676 self.add_source_with_metadata(name, arg, kind, None, false, 0)
677 }
678
679 pub fn add_source_with_metadata(
680 &mut self,
681 name: &str,
682 arg: &str,
683 kind: SourceKind,
684 trust_level: Option<&str>,
685 explicit: bool,
686 priority: i32,
687 ) -> Result<()> {
688 if self.use_system_winget_sources {
689 system_winget_add_source(name, arg, kind, &normalize_source_trust_level(trust_level)?, explicit)?;
690 self.refresh_system_winget_sources()?;
691 return Ok(());
692 }
693
694 if self.store.sources.iter().any(|s| s.name.eq_ignore_ascii_case(name)) {
695 bail!("A source with name '{name}' already exists.");
696 }
697 if self.store.sources.iter().any(|s| s.arg == arg) {
698 bail!("A source with argument '{arg}' already exists.");
699 }
700 let identifier = name.to_owned();
701 self.store.sources.push(SourceRecord {
702 name: name.to_owned(),
703 kind,
704 arg: arg.to_owned(),
705 identifier,
706 trust_level: normalize_source_trust_level(trust_level)?,
707 explicit,
708 priority,
709 last_update: None,
710 source_version: None,
711 });
712 self.save_store()?;
713 Ok(())
714 }
715
716 pub fn edit_source(&mut self, name: &str, explicit: Option<bool>, trust_level: Option<&str>) -> Result<()> {
717 if self.use_system_winget_sources {
718 bail!("Editing system WinGet sources is not supported by winget source commands.");
719 }
720
721 let source = self
722 .store
723 .sources
724 .iter_mut()
725 .find(|source| source.name.eq_ignore_ascii_case(name))
726 .ok_or_else(|| anyhow!("Source '{name}' not found."))?;
727
728 if let Some(explicit) = explicit {
729 source.explicit = explicit;
730 }
731
732 if trust_level.is_some() {
733 source.trust_level = normalize_source_trust_level(trust_level)?;
734 }
735
736 self.save_store()?;
737 Ok(())
738 }
739
740 pub fn remove_source(&mut self, name: &str) -> Result<()> {
741 if self.use_system_winget_sources {
742 system_winget_remove_source(name)?;
743 self.refresh_system_winget_sources()?;
744 return Ok(());
745 }
746
747 let idx = self
748 .store
749 .sources
750 .iter()
751 .position(|s| s.name.eq_ignore_ascii_case(name))
752 .ok_or_else(|| anyhow!("Source '{name}' not found."))?;
753 let source = self.store.sources.remove(idx);
754 let state_dir = self.source_state_dir(&source);
756 if state_dir.exists() {
757 let _ = fs::remove_dir_all(&state_dir);
758 }
759 self.save_store()?;
760 Ok(())
761 }
762
763 pub fn reset_source(&mut self, name: &str) -> Result<()> {
764 if self.use_system_winget_sources {
765 system_winget_reset_source(name)?;
766 self.refresh_system_winget_sources()?;
767 return Ok(());
768 }
769
770 let idx = self
771 .store
772 .sources
773 .iter()
774 .position(|source| source.name.eq_ignore_ascii_case(name))
775 .ok_or_else(|| anyhow!("Source '{name}' not found."))?;
776
777 let source = self.store.sources[idx].clone();
778 let state_dir = self.source_state_dir(&source);
779 if state_dir.exists() {
780 let _ = fs::remove_dir_all(&state_dir);
781 }
782
783 if let Some(default_source) = SourceStore::default()
784 .sources
785 .into_iter()
786 .find(|source| source.name.eq_ignore_ascii_case(name))
787 {
788 self.store.sources[idx] = default_source;
789 } else {
790 self.store.sources[idx].last_update = None;
791 self.store.sources[idx].source_version = None;
792 }
793
794 self.save_store()?;
795 Ok(())
796 }
797
798 pub fn reset_sources(&mut self) -> Result<()> {
799 if self.use_system_winget_sources {
800 system_winget_reset_sources()?;
801 self.refresh_system_winget_sources()?;
802 return Ok(());
803 }
804
805 for source in &self.store.sources {
807 let state_dir = self.source_state_dir(source);
808 if state_dir.exists() {
809 let _ = fs::remove_dir_all(&state_dir);
810 }
811 }
812 self.store = SourceStore::default();
813 self.save_store()?;
814 Ok(())
815 }
816
817 pub fn supported_admin_settings() -> &'static [&'static str] {
818 SUPPORTED_ADMIN_SETTINGS
819 }
820
821 pub fn get_user_settings(&self) -> Result<JsonValue> {
822 Ok(JsonValue::Object(load_json_object(&user_settings_path(
823 &self.app_root,
824 ))?))
825 }
826
827 pub fn set_user_settings(&self, user_settings: &JsonValue, merge: bool) -> Result<JsonValue> {
828 let update = match user_settings {
829 JsonValue::Object(object) => object.clone(),
830 _ => bail!("user settings must be a JSON object"),
831 };
832
833 let effective = if merge {
834 merge_json_objects(&load_json_object(&user_settings_path(&self.app_root))?, &update)
835 } else {
836 update
837 };
838 save_json_object(user_settings_path(&self.app_root), &effective)?;
839 Ok(JsonValue::Object(effective))
840 }
841
842 pub fn test_user_settings(&self, expected: &JsonValue, ignore_not_set: bool) -> Result<bool> {
843 let current = self.get_user_settings()?;
844 Ok(if ignore_not_set {
845 json_contains(¤t, expected)
846 } else {
847 current == *expected
848 })
849 }
850
851 pub fn get_admin_settings(&self) -> Result<JsonValue> {
852 let mut settings = load_json_object(&admin_settings_path(&self.app_root))?;
853 for name in SUPPORTED_ADMIN_SETTINGS {
854 settings.entry((*name).to_owned()).or_insert(JsonValue::Bool(false));
855 }
856
857 Ok(JsonValue::Object(settings))
858 }
859
860 pub fn set_admin_setting(&self, name: &str, enabled: bool) -> Result<()> {
861 let normalized = normalize_admin_setting_name(name)?;
862 let mut settings = match self.get_admin_settings()? {
863 JsonValue::Object(settings) => settings,
864 _ => JsonMap::new(),
865 };
866 settings.insert(normalized.to_owned(), JsonValue::Bool(enabled));
867 save_json_object(admin_settings_path(&self.app_root), &settings)
868 }
869
870 pub fn reset_admin_setting(&self, name: Option<&str>, reset_all: bool) -> Result<()> {
871 if !reset_all && name.is_none() {
872 bail!("resetting admin settings requires a setting name or reset-all");
873 }
874
875 let mut settings = match self.get_admin_settings()? {
876 JsonValue::Object(settings) => settings,
877 _ => JsonMap::new(),
878 };
879
880 if reset_all {
881 for setting_name in SUPPORTED_ADMIN_SETTINGS {
882 settings.insert((*setting_name).to_owned(), JsonValue::Bool(false));
883 }
884 } else {
885 settings.insert(
886 normalize_admin_setting_name(name.expect("checked above"))?.to_owned(),
887 JsonValue::Bool(false),
888 );
889 }
890
891 save_json_object(admin_settings_path(&self.app_root), &settings)
892 }
893
894 pub fn ensure_settings_files(&self) -> Result<()> {
895 let user_settings = user_settings_path(&self.app_root);
896 if !user_settings.exists() {
897 save_json_object(user_settings, &JsonMap::new())?;
898 }
899
900 let admin_settings = admin_settings_path(&self.app_root);
901 if !admin_settings.exists() {
902 let mut values = JsonMap::new();
903 for name in SUPPORTED_ADMIN_SETTINGS {
904 values.insert((*name).to_owned(), JsonValue::Bool(false));
905 }
906 save_json_object(admin_settings, &values)?;
907 }
908
909 Ok(())
910 }
911
912 pub fn update_sources(&mut self, source_name: Option<&str>) -> Result<Vec<SourceUpdateResult>> {
913 if self.use_system_winget_sources {
914 return self.update_system_winget_sources(source_name);
915 }
916
917 let indexes = self.resolve_source_indexes(source_name)?;
918 let mut results = Vec::new();
919
920 for index in indexes {
921 let detail = match self.store.sources[index].kind {
922 SourceKind::PreIndexed => self.update_preindexed(index)?,
923 SourceKind::Rest => self.update_rest(index)?,
924 };
925
926 results.push(SourceUpdateResult {
927 name: self.store.sources[index].name.clone(),
928 kind: self.store.sources[index].kind,
929 detail,
930 });
931 }
932
933 self.save_store()?;
934 Ok(results)
935 }
936
937 pub fn search(&mut self, query: &PackageQuery) -> Result<SearchResponse> {
938 let (matches, warnings, truncated) = self.search_located(query, SearchSemantics::Many)?;
939 Ok(SearchResponse {
940 matches: matches.into_iter().map(|item| item.display).collect(),
941 warnings,
942 truncated,
943 })
944 }
945
946 pub fn search_manifests(&mut self, query: &PackageQuery) -> Result<Vec<JsonValue>> {
947 let (matches, _, _) = self.search_located(query, SearchSemantics::Many)?;
948 let mut structured_documents = Vec::with_capacity(matches.len());
949 for located in matches {
950 let (_, manifest_documents, _) = self.manifest_for_match(&located, query)?;
951 structured_documents.push(manifest_documents);
952 }
953
954 Ok(collapse_structured_documents(&structured_documents))
955 }
956
957 pub fn list(&mut self, query: &ListQuery) -> Result<ListResponse> {
958 if (query.include_unknown || query.include_pinned) && !query.upgrade_only {
959 bail!("--include-unknown and --include-pinned require --upgrade-available");
960 }
961 if query.source.is_some()
962 && query.query.is_none()
963 && query.id.is_none()
964 && query.name.is_none()
965 && query.moniker.is_none()
966 && query.tag.is_none()
967 && query.command.is_none()
968 {
969 bail!("list --source currently requires a query or explicit filter");
970 }
971
972 let has_filter = list_query_needs_available_lookup(query);
973 let needs_available = has_filter || query.upgrade_only;
974
975 let mut warnings = Vec::new();
976 if !installed_package_discovery_supported() {
977 warnings.push(INSTALLED_STATE_UNSUPPORTED_WARNING.to_owned());
978 }
979 let mut installed = collect_installed_packages(query.install_scope.as_deref())?;
980
981 if needs_available && has_filter {
982 let available_query = package_query_from_list_query(query);
984 let (matches, source_warnings, _) = self.search_located(&available_query, SearchSemantics::Many)?;
985 warnings.extend(source_warnings);
986 let candidates: Vec<SearchMatch> = matches.into_iter().map(|c| c.display).collect();
987 for package in &mut installed {
988 package.correlated =
989 correlate_installed_package(package, &candidates, allow_loose_list_correlation(query));
990 }
991 } else if needs_available {
992 warnings.extend(self.correlate_all_installed(&mut installed)?);
994 }
995
996 let mut matches = installed
997 .into_iter()
998 .filter(|package| {
999 list_package_matches(package, query)
1000 && (!query.upgrade_only || installed_package_matches_upgrade_filter(package, query))
1001 })
1002 .collect::<Vec<_>>();
1003
1004 matches.sort_by(|left, right| {
1005 list_sort_weight(left)
1006 .cmp(&list_sort_weight(right))
1007 .then_with(|| left.name.to_ascii_lowercase().cmp(&right.name.to_ascii_lowercase()))
1008 .then_with(|| left.local_id.cmp(&right.local_id))
1009 });
1010
1011 let pins = if query.upgrade_only {
1012 self.list_pins(query.source.as_deref())?
1013 } else {
1014 Vec::new()
1015 };
1016 let mut list_matches = matches
1017 .into_iter()
1018 .map(list_match_from_installed)
1019 .filter(|item| !query.upgrade_only || query.include_pinned || !is_upgrade_blocked_by_pin(item, &pins))
1020 .collect::<Vec<_>>();
1021 let truncated = if let Some(limit) = query.count {
1022 let was_truncated = list_matches.len() > limit;
1023 list_matches.truncate(limit);
1024 was_truncated
1025 } else {
1026 false
1027 };
1028
1029 Ok(ListResponse {
1030 matches: list_matches,
1031 warnings,
1032 truncated,
1033 })
1034 }
1035
1036 fn correlate_all_installed(&mut self, installed: &mut [InstalledPackage]) -> Result<Vec<String>> {
1039 let all_query = PackageQuery {
1040 query: None,
1041 id: None,
1042 name: None,
1043 moniker: None,
1044 tag: None,
1045 command: None,
1046 source: None,
1047 count: Some(100_000), exact: false,
1049 version: None,
1050 channel: None,
1051 locale: None,
1052 installer_type: None,
1053 installer_architecture: None,
1054 platform: None,
1055 os_version: None,
1056 install_scope: None,
1057 };
1058 let (matches, warnings, _) = self.search_located(&all_query, SearchSemantics::Many)?;
1059 let candidates: Vec<SearchMatch> = matches.into_iter().map(|c| c.display).collect();
1060
1061 for package in installed.iter_mut() {
1062 package.correlated = correlate_installed_package(package, &candidates, true);
1063 }
1064
1065 Ok(warnings)
1066 }
1067
1068 pub fn search_versions(&mut self, query: &PackageQuery) -> Result<VersionsResult> {
1069 let (located, warnings) = self.find_single_match_with_semantics(query, SearchSemantics::Many)?;
1070 let versions = self.versions_for_match(&located, query)?;
1071 Ok(VersionsResult {
1072 package: located.display,
1073 versions,
1074 warnings,
1075 })
1076 }
1077
1078 pub fn show_versions(&mut self, query: &PackageQuery) -> Result<VersionsResult> {
1079 let (located, warnings) = self.find_single_match(query)?;
1080 let versions = self.versions_for_match(&located, query)?;
1081 Ok(VersionsResult {
1082 package: located.display,
1083 versions,
1084 warnings,
1085 })
1086 }
1087
1088 pub fn show(&mut self, query: &PackageQuery) -> Result<ShowResult> {
1089 let (located, warnings) = self.find_single_match(query)?;
1090 let (manifest, manifest_documents, cached_files) = self.manifest_for_match(&located, query)?;
1091 let selected_installer = select_installer(&manifest.installers, query);
1092
1093 Ok(ShowResult {
1094 package: located.display,
1095 manifest,
1096 selected_installer,
1097 cached_files,
1098 warnings,
1099 manifest_documents,
1100 })
1101 }
1102
1103 pub fn warm_cache(&mut self, query: &PackageQuery) -> Result<CacheWarmResult> {
1104 let (located, warnings) = self.find_single_match(query)?;
1105 let (_, _, cached_files) = self.manifest_for_match(&located, query)?;
1106
1107 Ok(CacheWarmResult {
1108 package: located.display,
1109 cached_files,
1110 warnings,
1111 })
1112 }
1113
1114 pub fn list_pins(&self, source_id: Option<&str>) -> Result<Vec<PinRecord>> {
1117 let db_path = pins_db_path(&self.app_root);
1118 if !db_path.exists() {
1119 return Ok(Vec::new());
1120 }
1121 let conn = Connection::open_with_flags(&db_path, OpenFlags::SQLITE_OPEN_READ_ONLY)?;
1122 let pin_type_column = match resolve_pin_type_column(&conn)? {
1123 Some(column) => column,
1124 None => return Ok(Vec::new()),
1125 };
1126 let sql = if source_id.is_some() {
1127 format!("SELECT package_id, version, source_id, {pin_type_column} FROM pin WHERE source_id = ?1")
1128 } else {
1129 format!("SELECT package_id, version, source_id, {pin_type_column} FROM pin")
1130 };
1131 let mut stmt = conn.prepare(&sql)?;
1132 let rows = if let Some(source_id) = source_id {
1133 stmt.query_map([source_id], |row| {
1134 let pin_type_int: i64 = row.get(3)?;
1135 Ok(PinRecord {
1136 package_id: row.get(0)?,
1137 version: row.get(1)?,
1138 source_id: row.get(2)?,
1139 pin_type: decode_pin_type(pin_type_int),
1140 })
1141 })?
1142 .filter_map(|r| r.ok())
1143 .collect()
1144 } else {
1145 stmt.query_map([], |row| {
1146 let pin_type_int: i64 = row.get(3)?;
1147 Ok(PinRecord {
1148 package_id: row.get(0)?,
1149 version: row.get(1)?,
1150 source_id: row.get(2)?,
1151 pin_type: decode_pin_type(pin_type_int),
1152 })
1153 })?
1154 .filter_map(|r| r.ok())
1155 .collect()
1156 };
1157 Ok(rows)
1158 }
1159
1160 pub fn add_pin(&self, package_id: &str, version: &str, source_id: &str, pin_type: PinType) -> Result<()> {
1161 let db_path = pins_db_path(&self.app_root);
1162 let conn = Connection::open(&db_path)?;
1163 conn.execute_batch(
1164 "CREATE TABLE IF NOT EXISTS pin (
1165 package_id TEXT NOT NULL,
1166 version TEXT NOT NULL DEFAULT '*',
1167 source_id TEXT NOT NULL DEFAULT '',
1168 type INTEGER NOT NULL DEFAULT 0,
1169 PRIMARY KEY (package_id, source_id)
1170 )",
1171 )?;
1172 let pin_type_column = resolve_pin_type_column(&conn)?.unwrap_or("type");
1173 let type_int: i64 = match pin_type {
1174 PinType::Pinning => 2,
1175 PinType::Blocking => 4,
1176 PinType::Gating => 3,
1177 };
1178 conn.execute(
1179 &format!(
1180 "INSERT OR REPLACE INTO pin (package_id, version, source_id, {pin_type_column}) VALUES (?1, ?2, ?3, ?4)"
1181 ),
1182 rusqlite::params![package_id, version, source_id, type_int],
1183 )?;
1184 Ok(())
1185 }
1186
1187 pub fn remove_pin(&self, package_id: &str, source_id: Option<&str>) -> Result<bool> {
1188 let db_path = pins_db_path(&self.app_root);
1189 if !db_path.exists() {
1190 return Ok(false);
1191 }
1192 let conn = Connection::open(&db_path)?;
1193 let count = if let Some(source_id) = source_id {
1194 conn.execute(
1195 "DELETE FROM pin WHERE package_id = ?1 AND source_id = ?2",
1196 rusqlite::params![package_id, source_id],
1197 )?
1198 } else {
1199 conn.execute("DELETE FROM pin WHERE package_id = ?1", rusqlite::params![package_id])?
1200 };
1201 Ok(count > 0)
1202 }
1203
1204 pub fn reset_pins(&self, source_id: Option<&str>) -> Result<()> {
1205 let db_path = pins_db_path(&self.app_root);
1206 if !db_path.exists() {
1207 return Ok(());
1208 }
1209
1210 if let Some(source_id) = source_id {
1211 let conn = Connection::open(&db_path)?;
1212 conn.execute("DELETE FROM pin WHERE source_id = ?1", rusqlite::params![source_id])?;
1213 } else {
1214 fs::remove_file(&db_path)?;
1215 }
1216 Ok(())
1217 }
1218
1219 fn save_store(&self) -> Result<()> {
1220 save_store(&self.app_root, &self.store)
1221 }
1222
1223 fn source_state_dir(&self, source: &SourceRecord) -> PathBuf {
1224 source_state_dir(&self.app_root, source)
1225 }
1226
1227 pub fn download_installer(&mut self, query: &PackageQuery, download_dir: &Path) -> Result<(Manifest, PathBuf)> {
1230 self.download_installer_for_request(&InstallRequest::new(query.clone()), download_dir)
1231 }
1232
1233 pub fn download_installer_for_request(
1234 &mut self,
1235 request: &InstallRequest,
1236 download_dir: &Path,
1237 ) -> Result<(Manifest, PathBuf)> {
1238 let manifest = self.resolve_manifest_for_install(request)?;
1239 let installer = select_installer(&manifest.installers, &request.query)
1240 .ok_or_else(|| anyhow!("No applicable installer found for the current system"))?;
1241 let url = installer
1242 .url
1243 .as_deref()
1244 .ok_or_else(|| anyhow!("Installer has no URL"))?;
1245
1246 fs::create_dir_all(download_dir)?;
1247 let filename = request.rename.as_deref().unwrap_or_else(|| {
1248 url.rsplit('/')
1249 .next()
1250 .unwrap_or("installer")
1251 .split('?')
1252 .next()
1253 .unwrap_or("installer")
1254 });
1255 let dest = download_dir.join(filename);
1256
1257 let response = self.client.get(url).send().context("failed to download installer")?;
1258 if !response.status().is_success() {
1259 bail!("Download failed: HTTP {}", response.status());
1260 }
1261 let bytes = response.bytes()?;
1262 fs::write(&dest, &bytes)?;
1263
1264 if let Err(error) = verify_installer_hash(installer.sha256.as_deref(), &bytes, request.ignore_security_hash) {
1265 let _ = fs::remove_file(&dest);
1266 return Err(error);
1267 }
1268
1269 Ok((manifest, dest))
1270 }
1271
1272 pub fn install(&mut self, query: &PackageQuery, silent: bool) -> Result<InstallResult> {
1273 let mut request = InstallRequest::new(query.clone());
1274 request.mode = if silent {
1275 InstallerMode::Silent
1276 } else {
1277 InstallerMode::SilentWithProgress
1278 };
1279 self.install_request(&request)
1280 }
1281
1282 pub fn install_with_mode(&mut self, query: &PackageQuery, mode: InstallerMode) -> Result<InstallResult> {
1283 let mut request = InstallRequest::new(query.clone());
1284 request.mode = mode;
1285 self.install_request(&request)
1286 }
1287
1288 pub fn install_request(&mut self, request: &InstallRequest) -> Result<InstallResult> {
1289 let manifest = self.resolve_manifest_for_install(request)?;
1290 if !package_actions_supported() {
1291 let installer_type = select_installer(&manifest.installers, &request.query)
1292 .and_then(|installer| installer.installer_type)
1293 .unwrap_or_else(|| "install".to_owned())
1294 .to_lowercase();
1295 return Ok(unsupported_action_result(
1296 manifest.id.clone(),
1297 manifest.version.clone(),
1298 installer_type,
1299 INSTALL_UNSUPPORTED_WARNING,
1300 ));
1301 }
1302
1303 let existing_match = self.find_installed_package_for_install(request, &manifest)?;
1304 if let Some(no_op_result) = Self::create_install_no_op_result(request, &manifest, existing_match.as_ref()) {
1305 return Ok(no_op_result);
1306 }
1307
1308 Self::ensure_package_agreements_accepted(&manifest, request)?;
1309 self.install_dependencies(&manifest, request, &mut HashSet::new())?;
1310
1311 if request.dependencies_only {
1312 return Ok(InstallResult {
1313 package_id: manifest.id.clone(),
1314 version: manifest.version.clone(),
1315 installer_path: PathBuf::new(),
1316 installer_type: "dependencies".to_owned(),
1317 exit_code: 0,
1318 success: true,
1319 no_op: false,
1320 warnings: Vec::new(),
1321 });
1322 }
1323
1324 let installer = select_installer(&manifest.installers, &request.query)
1325 .ok_or_else(|| anyhow!("No applicable installer found"))?;
1326
1327 if request.uninstall_previous {
1328 let mut uninstall_request = UninstallRequest::new(PackageQuery {
1329 id: request.query.id.clone().or_else(|| Some(manifest.id.clone())),
1330 query: request.query.query.clone().or_else(|| Some(manifest.id.clone())),
1331 source: request.query.source.clone(),
1332 exact: true,
1333 version: request.query.version.clone(),
1334 ..PackageQuery::default()
1335 });
1336 uninstall_request.product_code = installer.product_code.clone();
1337 uninstall_request.mode = InstallerMode::Silent;
1338 uninstall_request.all_versions = true;
1339 uninstall_request.force = true;
1340 let _ = self.uninstall_request(&uninstall_request);
1341 }
1342
1343 let temp_dir = std::env::temp_dir().join("pinget-install");
1344 let (_, installer_path) = self.download_installer_for_request(request, &temp_dir)?;
1345
1346 let installer_type = installer.installer_type.as_deref().unwrap_or("exe").to_lowercase();
1347
1348 let exit_code = dispatch_installer(&installer_path, &installer_type, request, &manifest, &installer)?;
1349
1350 Ok(InstallResult {
1351 package_id: manifest.id.clone(),
1352 version: manifest.version.clone(),
1353 installer_path,
1354 installer_type,
1355 exit_code,
1356 success: exit_code == 0,
1357 no_op: false,
1358 warnings: Vec::new(),
1359 })
1360 }
1361
1362 fn find_installed_package_for_install(
1363 &mut self,
1364 request: &InstallRequest,
1365 manifest: &Manifest,
1366 ) -> Result<Option<ListMatch>> {
1367 let installed_matches = self.list(&ListQuery {
1368 query: request.query.query.clone(),
1369 id: request.query.id.clone().or_else(|| Some(manifest.id.clone())),
1370 name: request.query.name.clone(),
1371 moniker: request.query.moniker.clone(),
1372 tag: None,
1373 command: None,
1374 product_code: None,
1375 version: None,
1376 source: request.query.source.clone(),
1377 count: Some(100),
1378 exact: request.query.exact || !manifest.id.trim().is_empty(),
1379 install_scope: request.query.install_scope.clone(),
1380 upgrade_only: false,
1381 include_unknown: false,
1382 include_pinned: false,
1383 })?;
1384
1385 Ok(installed_matches.matches.into_iter().find(|candidate| {
1386 candidate.id.eq_ignore_ascii_case(&manifest.id) || candidate.local_id.eq_ignore_ascii_case(&manifest.id)
1387 }))
1388 }
1389
1390 fn create_install_no_op_result(
1391 request: &InstallRequest,
1392 manifest: &Manifest,
1393 existing_match: Option<&ListMatch>,
1394 ) -> Option<InstallResult> {
1395 let existing_match = existing_match?;
1396
1397 if request.no_upgrade {
1398 return Some(InstallResult {
1399 package_id: manifest.id.clone(),
1400 version: existing_match.installed_version.clone(),
1401 installer_path: PathBuf::new(),
1402 installer_type: "install".to_owned(),
1403 exit_code: 0,
1404 success: true,
1405 no_op: true,
1406 warnings: vec!["Package is already installed; skipping because --no-upgrade was specified.".to_owned()],
1407 });
1408 }
1409
1410 if !request.force
1411 && !existing_match.installed_version.trim().is_empty()
1412 && compare_version(&existing_match.installed_version, &manifest.version) != Ordering::Less
1413 {
1414 return Some(InstallResult {
1415 package_id: manifest.id.clone(),
1416 version: existing_match.installed_version.clone(),
1417 installer_path: PathBuf::new(),
1418 installer_type: "install".to_owned(),
1419 exit_code: 0,
1420 success: true,
1421 no_op: true,
1422 warnings: vec![
1423 "Package is already installed and up to date; rerun with --force to reinstall.".to_owned(),
1424 ],
1425 });
1426 }
1427
1428 None
1429 }
1430
1431 pub fn repair(&mut self, request: &RepairRequest) -> Result<InstallResult> {
1432 if request.manifest_path.is_none() && !package_actions_supported() {
1433 let package_id = request
1434 .query
1435 .id
1436 .clone()
1437 .or_else(|| request.query.name.clone())
1438 .or_else(|| request.query.moniker.clone())
1439 .or_else(|| request.query.query.clone())
1440 .or_else(|| request.product_code.clone())
1441 .unwrap_or_else(|| "repair".to_owned());
1442 return Ok(unsupported_action_result(
1443 package_id,
1444 request.query.version.clone().unwrap_or_default(),
1445 "repair".to_owned(),
1446 REPAIR_UNSUPPORTED_WARNING,
1447 ));
1448 }
1449
1450 let mut warnings = vec![REPAIR_REINSTALL_WARNING.to_owned()];
1451 let installed_match = if request.manifest_path.is_some() {
1452 None
1453 } else {
1454 let installed = self.list(&Self::create_repair_list_query(request))?;
1455 warnings.extend(installed.warnings);
1456 if installed.matches.is_empty() {
1457 bail!("No installed package matched the supplied repair query.");
1458 }
1459 if installed.matches.len() > 1 {
1460 bail!("Multiple installed packages matched the supplied repair query.");
1461 }
1462 installed.matches.into_iter().next()
1463 };
1464
1465 let mut result =
1466 self.install_request(&Self::create_repair_install_request(request, installed_match.as_ref()))?;
1467 warnings.extend(result.warnings);
1468 result.warnings = warnings;
1469 Ok(result)
1470 }
1471
1472 pub fn uninstall(&mut self, query: &PackageQuery, silent: bool) -> Result<InstallResult> {
1473 let mut request = UninstallRequest::new(query.clone());
1474 request.mode = if silent {
1475 InstallerMode::Silent
1476 } else {
1477 InstallerMode::SilentWithProgress
1478 };
1479 self.uninstall_request(&request)
1480 }
1481
1482 pub fn uninstall_request(&mut self, request: &UninstallRequest) -> Result<InstallResult> {
1483 if !package_actions_supported() {
1484 let (package_id, version) = if let Some(path) = &request.manifest_path {
1485 let manifest = Self::load_manifest_from_path(path)?;
1486 (manifest.id, request.query.version.clone().unwrap_or(manifest.version))
1487 } else {
1488 (
1489 request
1490 .query
1491 .id
1492 .clone()
1493 .or_else(|| request.query.query.clone())
1494 .or_else(|| request.query.name.clone())
1495 .or_else(|| request.query.moniker.clone())
1496 .or_else(|| request.product_code.clone())
1497 .unwrap_or_else(|| "unknown".to_owned()),
1498 request.query.version.clone().unwrap_or_default(),
1499 )
1500 };
1501
1502 return Ok(unsupported_action_result(
1503 package_id,
1504 version,
1505 "uninstall",
1506 UNINSTALL_UNSUPPORTED_WARNING,
1507 ));
1508 }
1509
1510 let matches = self.resolve_uninstall_matches(request)?;
1511 let mut exit_code = 0;
1512
1513 for installed in &matches {
1514 exit_code = uninstall_package(installed, request)?;
1515 if exit_code != 0 && !request.all_versions {
1516 break;
1517 }
1518 }
1519
1520 let installed = matches
1521 .first()
1522 .ok_or_else(|| anyhow!("No installed package found matching the query"))?;
1523
1524 Ok(InstallResult {
1525 package_id: installed.id.clone(),
1526 version: installed.installed_version.clone(),
1527 installer_path: PathBuf::new(),
1528 installer_type: "uninstall".to_owned(),
1529 exit_code,
1530 success: exit_code == 0,
1531 no_op: false,
1532 warnings: Vec::new(),
1533 })
1534 }
1535
1536 fn resolve_manifest_for_install(&mut self, request: &InstallRequest) -> Result<Manifest> {
1537 if let Some(path) = &request.manifest_path {
1538 return Self::load_manifest_from_path(path);
1539 }
1540
1541 let (located, _warnings) = self.find_single_match(&request.query)?;
1542 let (manifest, _, _cached_files) = self.manifest_for_match(&located, &request.query)?;
1543 Ok(manifest)
1544 }
1545
1546 fn ensure_package_agreements_accepted(manifest: &Manifest, request: &InstallRequest) -> Result<()> {
1547 if !request.accept_package_agreements && !manifest.agreements.is_empty() {
1548 bail!("Package agreements are present; rerun with --accept-package-agreements to continue.");
1549 }
1550 Ok(())
1551 }
1552
1553 fn install_dependencies(
1554 &mut self,
1555 manifest: &Manifest,
1556 request: &InstallRequest,
1557 visited: &mut HashSet<String>,
1558 ) -> Result<()> {
1559 if request.skip_dependencies {
1560 return Ok(());
1561 }
1562
1563 let dependency_ids = manifest
1564 .package_dependencies
1565 .iter()
1566 .chain(
1567 manifest
1568 .installers
1569 .iter()
1570 .flat_map(|installer| installer.package_dependencies.iter()),
1571 )
1572 .filter(|value| !value.trim().is_empty())
1573 .cloned()
1574 .collect::<BTreeSet<_>>();
1575
1576 for dependency_id in dependency_ids {
1577 if !visited.insert(dependency_id.clone()) {
1578 continue;
1579 }
1580
1581 let dependency_request = Self::create_dependency_install_request(dependency_id, request);
1582 self.install_request(&dependency_request)?;
1583 }
1584
1585 Ok(())
1586 }
1587
1588 fn create_dependency_install_request(dependency_id: String, request: &InstallRequest) -> InstallRequest {
1589 let mut dependency_request = InstallRequest::new(PackageQuery {
1590 id: Some(dependency_id),
1591 source: request
1592 .dependency_source
1593 .clone()
1594 .or_else(|| request.query.source.clone()),
1595 exact: true,
1596 ..PackageQuery::default()
1597 });
1598 dependency_request.mode = match request.mode {
1599 InstallerMode::Interactive => InstallerMode::Interactive,
1600 InstallerMode::SilentWithProgress | InstallerMode::Silent => InstallerMode::Silent,
1601 };
1602 dependency_request.accept_package_agreements = request.accept_package_agreements;
1603 dependency_request.no_upgrade = true;
1604 dependency_request.ignore_security_hash = request.ignore_security_hash;
1605 dependency_request.dependency_source = request.dependency_source.clone();
1606 dependency_request
1607 }
1608
1609 fn create_repair_list_query(request: &RepairRequest) -> ListQuery {
1610 ListQuery {
1611 query: request.query.query.clone(),
1612 id: request.query.id.clone(),
1613 name: request.query.name.clone(),
1614 moniker: request.query.moniker.clone(),
1615 product_code: request.product_code.clone(),
1616 version: request.query.version.clone(),
1617 source: request.query.source.clone(),
1618 count: Some(100),
1619 exact: request.query.exact,
1620 install_scope: request.query.install_scope.clone(),
1621 ..ListQuery::default()
1622 }
1623 }
1624
1625 fn create_repair_install_request(request: &RepairRequest, installed_match: Option<&ListMatch>) -> InstallRequest {
1626 let mut query = request.query.clone();
1627 query.query = if installed_match.is_none() {
1628 request.query.query.clone()
1629 } else {
1630 None
1631 };
1632 query.id = installed_match
1633 .map(|item| item.id.clone())
1634 .or_else(|| request.query.id.clone());
1635 query.name = if installed_match.is_none() {
1636 request.query.name.clone()
1637 } else {
1638 None
1639 };
1640 query.moniker = if installed_match.is_none() {
1641 request.query.moniker.clone()
1642 } else {
1643 None
1644 };
1645 query.source = request
1646 .query
1647 .source
1648 .clone()
1649 .or_else(|| installed_match.and_then(|item| item.source_name.clone()));
1650 query.exact = true;
1651 query.version = request
1652 .query
1653 .version
1654 .clone()
1655 .or_else(|| installed_match.map(|item| item.installed_version.clone()));
1656
1657 InstallRequest {
1658 query,
1659 manifest_path: request.manifest_path.clone(),
1660 mode: request.mode,
1661 log_path: request.log_path.clone(),
1662 custom: None,
1663 override_args: None,
1664 install_location: None,
1665 skip_dependencies: false,
1666 dependencies_only: false,
1667 accept_package_agreements: request.accept_package_agreements,
1668 force: true,
1669 rename: None,
1670 uninstall_previous: false,
1671 ignore_security_hash: request.ignore_security_hash,
1672 dependency_source: None,
1673 no_upgrade: false,
1674 }
1675 }
1676
1677 fn resolve_uninstall_matches(&mut self, request: &UninstallRequest) -> Result<Vec<ListMatch>> {
1678 let mut effective_query = request.query.clone();
1679 let mut effective_product_code = request.product_code.clone();
1680
1681 if let Some(path) = &request.manifest_path {
1682 let manifest = Self::load_manifest_from_path(path)?;
1683 effective_query = PackageQuery {
1684 query: Some(manifest.id.clone()),
1685 id: Some(manifest.id.clone()),
1686 name: Some(manifest.name.clone()),
1687 exact: true,
1688 version: effective_query.version.clone().or(Some(manifest.version)),
1689 ..PackageQuery::default()
1690 };
1691 effective_product_code = effective_product_code.or_else(|| {
1692 manifest
1693 .installers
1694 .iter()
1695 .find_map(|installer| installer.product_code.clone())
1696 });
1697 }
1698
1699 let list_query = ListQuery {
1700 query: effective_query.query.clone(),
1701 id: effective_query.id.clone(),
1702 name: effective_query.name.clone(),
1703 moniker: effective_query.moniker.clone(),
1704 tag: None,
1705 command: None,
1706 product_code: effective_product_code,
1707 version: effective_query.version.clone(),
1708 source: effective_query.source.clone(),
1709 count: if request.all_versions { None } else { Some(100) },
1710 exact: effective_query.exact,
1711 install_scope: effective_query.install_scope,
1712 upgrade_only: false,
1713 include_unknown: false,
1714 include_pinned: false,
1715 };
1716
1717 let list_result = self.list(&list_query)?;
1718 if list_result.matches.is_empty() {
1719 bail!("No installed package found matching the query");
1720 }
1721 if !request.all_versions && list_result.matches.len() > 1 && !request.force {
1722 bail!("Multiple installed packages matched the query; refine the query or use --all-versions.");
1723 }
1724
1725 Ok(if request.all_versions {
1726 list_result.matches
1727 } else {
1728 vec![list_result.matches[0].clone()]
1729 })
1730 }
1731
1732 fn load_manifest_from_path(manifest_path: &Path) -> Result<Manifest> {
1733 let resolved = resolve_manifest_path(manifest_path)?;
1734 let bytes =
1735 fs::read(&resolved).with_context(|| format!("failed to read manifest from {}", resolved.display()))?;
1736 parse_yaml_manifest(&bytes)
1737 }
1738
1739 fn find_single_match(&mut self, query: &PackageQuery) -> Result<(LocatedMatch, Vec<String>)> {
1740 self.find_single_match_with_semantics(query, SearchSemantics::Single)
1741 }
1742
1743 fn find_single_match_with_semantics(
1744 &mut self,
1745 query: &PackageQuery,
1746 semantics: SearchSemantics,
1747 ) -> Result<(LocatedMatch, Vec<String>)> {
1748 let (matches, warnings, _) = self.search_located(query, semantics)?;
1749
1750 if matches.is_empty() {
1751 bail!("no package matched the supplied query");
1752 }
1753
1754 if matches.len() > 1 {
1755 let choices = matches
1756 .iter()
1757 .take(10)
1758 .map(|item| {
1759 format!(
1760 "{} [{}] ({})",
1761 item.display.name, item.display.id, item.display.source_name
1762 )
1763 })
1764 .collect::<Vec<_>>()
1765 .join(", ");
1766 bail!("multiple packages matched: {choices}");
1767 }
1768
1769 Ok((matches.into_iter().next().expect("one match"), warnings))
1770 }
1771
1772 fn search_located(
1773 &mut self,
1774 query: &PackageQuery,
1775 semantics: SearchSemantics,
1776 ) -> Result<(Vec<LocatedMatch>, Vec<String>, bool)> {
1777 let indexes = self.resolve_source_indexes(query.source.as_deref())?;
1778 let mut matches = Vec::new();
1779 let mut warnings = Vec::new();
1780 let mut truncated = false;
1781
1782 for index in indexes {
1783 match self.search_source(index, query, semantics) {
1784 Ok(mut source_matches) => {
1785 truncated |= source_matches.truncated;
1786 matches.append(&mut source_matches.matches);
1787 }
1788 Err(_error) => warnings.push(format!(
1789 "Failed when searching source; results will not be included: {}",
1790 self.store.sources[index].name
1791 )),
1792 }
1793 }
1794
1795 if semantics == SearchSemantics::Many {
1796 matches.sort_by(|left, right| {
1797 search_match_sort_score(&right.display, query).cmp(&search_match_sort_score(&left.display, query))
1798 });
1799 let limit = max_results(query);
1800 if matches.len() > limit {
1801 truncated = true;
1802 matches.truncate(limit);
1803 }
1804 }
1805
1806 Ok((matches, warnings, truncated))
1807 }
1808
1809 fn search_source(
1810 &mut self,
1811 source_index: usize,
1812 query: &PackageQuery,
1813 semantics: SearchSemantics,
1814 ) -> Result<SearchSourceMatches> {
1815 match self.store.sources[source_index].kind {
1816 SourceKind::PreIndexed => self.search_preindexed(source_index, query, semantics),
1817 SourceKind::Rest => self.search_rest(source_index, query, semantics),
1818 }
1819 }
1820
1821 fn versions_for_match(&mut self, located: &LocatedMatch, _query: &PackageQuery) -> Result<Vec<VersionKey>> {
1822 match &located.locator {
1823 MatchLocator::PreIndexedV1 { package_rowid } => {
1824 let source = self.source_clone(located.source_index);
1825 let connection = self.open_preindexed_connection(located.source_index)?;
1826 let versions = query_v1_versions(&connection, *package_rowid)?;
1827 let mut keys = versions
1828 .into_iter()
1829 .map(|row| VersionKey {
1830 version: row.version,
1831 channel: row.channel,
1832 })
1833 .collect::<Vec<_>>();
1834 sort_versions_desc(&mut keys);
1835 if keys.is_empty() {
1836 bail!("no versions found for {} in {}", located.display.id, source.name);
1837 }
1838 Ok(keys)
1839 }
1840 MatchLocator::PreIndexedV2 {
1841 package_rowid,
1842 package_hash,
1843 } => {
1844 let source = self.source_clone(located.source_index);
1845 let (entries, _) = self.load_v2_version_data(&source, *package_rowid, package_hash.as_str())?;
1846 let mut keys = entries
1847 .into_iter()
1848 .map(|entry| VersionKey {
1849 version: entry.version,
1850 channel: String::new(),
1851 })
1852 .collect::<Vec<_>>();
1853 sort_versions_desc(&mut keys);
1854 Ok(keys)
1855 }
1856 MatchLocator::Rest { versions, .. } => {
1857 let mut keys = versions.clone();
1858 sort_versions_desc(&mut keys);
1859 Ok(keys)
1860 }
1861 }
1862 }
1863
1864 fn manifest_for_match(
1865 &mut self,
1866 located: &LocatedMatch,
1867 query: &PackageQuery,
1868 ) -> Result<(Manifest, JsonValue, Vec<PathBuf>)> {
1869 match &located.locator {
1870 MatchLocator::PreIndexedV1 { package_rowid } => {
1871 let source = self.source_clone(located.source_index);
1872 let connection = self.open_preindexed_connection(located.source_index)?;
1873 let versions = query_v1_versions(&connection, *package_rowid)?;
1874 let selected = select_v1_version(&versions, query.version.as_deref(), query.channel.as_deref())?;
1875 let relative_path = resolve_v1_relative_path(&connection, selected.pathpart_id)?;
1876 let bytes =
1877 self.get_cached_source_file("V1_M", &source, &relative_path, selected.manifest_hash.as_deref())?;
1878 let (mut manifest, manifest_documents) = parse_yaml_manifest_bundle(&bytes.bytes)?;
1879 manifest.version = selected.version.clone();
1880 manifest.channel = selected.channel;
1881 Ok((manifest, manifest_documents, vec![bytes.path]))
1882 }
1883 MatchLocator::PreIndexedV2 {
1884 package_rowid,
1885 package_hash,
1886 } => {
1887 let source = self.source_clone(located.source_index);
1888 let (entries, version_data_file) =
1889 self.load_v2_version_data(&source, *package_rowid, package_hash.as_str())?;
1890 let selected = select_v2_version(&entries, query.version.as_deref())?;
1891 let manifest_bytes = self.get_cached_source_file(
1892 "V2_M",
1893 &source,
1894 &selected.manifest_relative_path,
1895 Some(selected.manifest_hash.as_str()),
1896 )?;
1897 let (mut manifest, manifest_documents) = parse_yaml_manifest_bundle(&manifest_bytes.bytes)?;
1898 manifest.version = selected.version;
1899 Ok((
1900 manifest,
1901 manifest_documents,
1902 vec![version_data_file, manifest_bytes.path],
1903 ))
1904 }
1905 MatchLocator::Rest { package_id, versions } => {
1906 let source = self.source_clone(located.source_index);
1907 let selected = select_rest_version(versions, query.version.as_deref(), query.channel.as_deref())?;
1908 let (bytes, cache_path) =
1909 self.get_or_fetch_rest_manifest(&source, package_id, &selected.version, &selected.channel)?;
1910 let (manifest, manifest_documents) =
1911 parse_rest_manifest(&bytes, package_id, &selected.version, &selected.channel)?;
1912 Ok((manifest, manifest_documents, vec![cache_path]))
1913 }
1914 }
1915 }
1916
1917 fn search_preindexed(
1918 &mut self,
1919 source_index: usize,
1920 query: &PackageQuery,
1921 semantics: SearchSemantics,
1922 ) -> Result<SearchSourceMatches> {
1923 let connection = self.open_preindexed_connection(source_index)?;
1924 let source = self.source_clone(source_index);
1925
1926 match query_v2_matches(&connection, query, semantics) {
1927 Ok((rows, truncated)) => Ok(SearchSourceMatches {
1928 truncated,
1929 matches: rows
1930 .into_iter()
1931 .map(|row| LocatedMatch {
1932 display: SearchMatch {
1933 source_name: source.name.clone(),
1934 source_kind: source.kind,
1935 id: row.id.clone(),
1936 name: row.name.clone(),
1937 moniker: row.moniker.clone(),
1938 version: Some(row.version.clone()),
1939 channel: None,
1940 match_criteria: row.match_criteria.clone(),
1941 },
1942 source_index,
1943 locator: MatchLocator::PreIndexedV2 {
1944 package_rowid: row.package_rowid,
1945 package_hash: row.package_hash,
1946 },
1947 })
1948 .collect(),
1949 }),
1950 Err(error) if can_fallback_to_v1(&error) => {
1951 let (rows, truncated) = query_v1_matches(&connection, query, semantics)?;
1952 let grouped = group_v1_rows(rows);
1953
1954 Ok(SearchSourceMatches {
1955 truncated,
1956 matches: grouped
1957 .into_iter()
1958 .map(|row| LocatedMatch {
1959 display: SearchMatch {
1960 source_name: source.name.clone(),
1961 source_kind: source.kind,
1962 id: row.id.clone(),
1963 name: row.name.clone(),
1964 moniker: row.moniker.clone(),
1965 version: Some(row.version.clone()),
1966 channel: Some(row.channel.clone()),
1967 match_criteria: row.match_criteria.clone(),
1968 },
1969 source_index,
1970 locator: MatchLocator::PreIndexedV1 {
1971 package_rowid: row.package_rowid,
1972 },
1973 })
1974 .collect(),
1975 })
1976 }
1977 Err(error) => Err(error),
1978 }
1979 }
1980
1981 fn search_rest(
1982 &mut self,
1983 source_index: usize,
1984 query: &PackageQuery,
1985 semantics: SearchSemantics,
1986 ) -> Result<SearchSourceMatches> {
1987 let source = self.source_clone(source_index);
1988 let info = self.load_rest_information(source_index)?;
1989 let contract = choose_contract(&info.server_supported_versions)
1990 .ok_or_else(|| anyhow!("no compatible REST contract for {}", source.name))?;
1991
1992 let url = format!("{}/manifestSearch", source.arg.trim_end_matches('/'));
1993 let body = build_rest_search_body(query, &info, semantics)?;
1994 let response = self
1995 .client
1996 .post(url)
1997 .header("Version", contract)
1998 .json(&body)
1999 .send()
2000 .context("REST search request failed")?
2001 .error_for_status()
2002 .context("REST search request returned an error")?;
2003 let json = response
2004 .json::<JsonValue>()
2005 .context("failed to parse REST search response")?;
2006 let data = json
2007 .get("Data")
2008 .and_then(JsonValue::as_array)
2009 .cloned()
2010 .unwrap_or_default();
2011 let max_results = source_fetch_results(query, semantics);
2012
2013 let mut results = Vec::new();
2014 for item in data {
2015 let package_id = json_string(&item, "PackageIdentifier")
2016 .ok_or_else(|| anyhow!("REST search result missing PackageIdentifier"))?;
2017 let package_name =
2018 json_string(&item, "PackageName").ok_or_else(|| anyhow!("REST search result missing PackageName"))?;
2019 let mut versions = parse_rest_versions(&item)?;
2020 sort_versions_desc(&mut versions);
2021 let latest = versions
2022 .first()
2023 .cloned()
2024 .ok_or_else(|| anyhow!("REST search result had no versions"))?;
2025
2026 results.push(LocatedMatch {
2027 display: SearchMatch {
2028 source_name: source.name.clone(),
2029 source_kind: source.kind,
2030 id: package_id.clone(),
2031 name: package_name,
2032 moniker: json_string(&item, "Moniker"),
2033 version: Some(latest.version.clone()),
2034 channel: if latest.channel.is_empty() {
2035 None
2036 } else {
2037 Some(latest.channel.clone())
2038 },
2039 match_criteria: rest_match_criteria(&item, query, semantics),
2040 },
2041 source_index,
2042 locator: MatchLocator::Rest { package_id, versions },
2043 });
2044 }
2045
2046 Ok(SearchSourceMatches {
2047 truncated: results.len() >= max_results,
2048 matches: results,
2049 })
2050 }
2051
2052 fn open_preindexed_connection(&mut self, source_index: usize) -> Result<Connection> {
2053 let source = self.source_clone(source_index);
2054 let index_path = preindexed_index_path(&self.app_root, &source);
2055 if !index_path.exists() {
2056 let _ = self.update_preindexed(source_index)?;
2057 if !self.use_system_winget_sources {
2058 self.save_store()?;
2059 }
2060 }
2061
2062 Self::open_sqlite_connection(index_path).context("failed to open preindexed index")
2063 }
2064
2065 fn open_sqlite_connection(path: PathBuf) -> Result<Connection> {
2066 let path_text = path.to_string_lossy().into_owned();
2067 Connection::open_with_flags(path, OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI)
2068 .with_context(|| format!("failed to open SQLite database at {path_text}"))
2069 }
2070
2071 fn update_preindexed(&mut self, source_index: usize) -> Result<String> {
2072 let source = &mut self.store.sources[source_index];
2073 let state_dir = source_state_dir(&self.app_root, source);
2074 fs::create_dir_all(&state_dir).context("failed to create source state directory")?;
2075
2076 let mut last_error = None;
2077 for candidate in PREINDEXED_CANDIDATES {
2078 let url = format!("{}/{}", source.arg.trim_end_matches('/'), candidate);
2079 match self.client.get(&url).send() {
2080 Ok(response) if response.status().is_success() => {
2081 let header_version = response
2082 .headers()
2083 .get("x-ms-meta-sourceversion")
2084 .and_then(|value| value.to_str().ok())
2085 .map(str::to_owned);
2086 let bytes = response.bytes().context("failed to read preindexed package bytes")?;
2087 let payload = bytes.to_vec();
2088 let index_bytes = extract_zip_member(&payload, "Public/index.db")
2089 .context("preindexed package did not contain Public/index.db")?;
2090
2091 fs::write(preindexed_package_path(&self.app_root, source), &payload)
2092 .context("failed to persist source package")?;
2093 fs::write(preindexed_index_path(&self.app_root, source), index_bytes)
2094 .context("failed to persist source index")?;
2095 source.last_update = Some(Utc::now());
2096 source.source_version = header_version;
2097 return Ok(format!("downloaded {}", candidate));
2098 }
2099 Ok(response) => {
2100 last_error = Some(anyhow!("candidate {} returned HTTP {}", candidate, response.status()));
2101 }
2102 Err(error) => {
2103 last_error = Some(anyhow!("candidate {} failed: {error}", candidate));
2104 }
2105 }
2106 }
2107
2108 Err(last_error.unwrap_or_else(|| anyhow!("no preindexed source candidate succeeded")))
2109 }
2110
2111 fn update_rest(&mut self, source_index: usize) -> Result<String> {
2112 let info = self.fetch_rest_information(source_index, true)?;
2113 let source = &mut self.store.sources[source_index];
2114 if !info.source_identifier.is_empty() {
2115 source.identifier = info.source_identifier.clone();
2116 }
2117 source.last_update = Some(Utc::now());
2118 source.source_version = choose_contract(&info.server_supported_versions).map(str::to_owned);
2119 Ok("refreshed information cache".to_owned())
2120 }
2121
2122 fn load_rest_information(&mut self, source_index: usize) -> Result<RestInformation> {
2123 let source = self.source_clone(source_index);
2124 let cache_path = rest_information_cache_path(&self.app_root, &source);
2125
2126 if cache_path.exists() {
2127 let cache = serde_json::from_slice::<RestInfoCache>(
2128 &fs::read(&cache_path).context("failed to read REST information cache")?,
2129 )
2130 .context("failed to parse REST information cache")?;
2131 if cache.expires_at > Utc::now() {
2132 return Ok(cache.value);
2133 }
2134 }
2135
2136 self.fetch_rest_information(source_index, true)
2137 }
2138
2139 fn fetch_rest_information(&mut self, source_index: usize, persist: bool) -> Result<RestInformation> {
2140 let source = self.source_clone(source_index);
2141 let url = format!("{}/information", source.arg.trim_end_matches('/'));
2142 let response = self
2143 .client
2144 .get(url)
2145 .send()
2146 .context("REST information request failed")?
2147 .error_for_status()
2148 .context("REST information request returned an error")?;
2149
2150 let max_age = cache_control_max_age(&response);
2151 let json = response
2152 .json::<JsonValue>()
2153 .context("failed to parse REST information response")?;
2154 let data = json
2155 .get("Data")
2156 .cloned()
2157 .ok_or_else(|| anyhow!("REST information response missing Data"))?;
2158 let info = serde_json::from_value::<RestInformation>(data)
2159 .context("failed to deserialize REST information payload")?;
2160
2161 if persist {
2162 let cache = RestInfoCache {
2163 expires_at: Utc::now() + Duration::seconds(i64::try_from(max_age).unwrap_or(i64::MAX)),
2164 value: info.clone(),
2165 };
2166 write_json(rest_information_cache_path(&self.app_root, &source), &cache)?;
2167 }
2168
2169 Ok(info)
2170 }
2171
2172 fn load_v2_version_data(
2173 &self,
2174 source: &SourceRecord,
2175 package_rowid: i64,
2176 package_hash: &str,
2177 ) -> Result<(Vec<PackageVersionDataEntry>, PathBuf)> {
2178 let connection = Self::open_sqlite_connection(preindexed_index_path(&self.app_root, source))
2179 .context("failed to reopen preindexed index for V2 version data")?;
2180 let package_hash = package_hash.to_ascii_lowercase();
2181 let package_id = query_optional_value(
2182 &connection,
2183 "SELECT id FROM packages WHERE rowid = ?1",
2184 vec![SqlValue::Integer(package_rowid)],
2185 |row| row_string(row, 0),
2186 )?
2187 .ok_or_else(|| anyhow!("failed to resolve package id for V2 version data"))?;
2188 let relative_path = package_version_data_relative_path(&package_id, &package_hash);
2189 let file = self.get_cached_source_file("V2_PVD", source, &relative_path, Some(&package_hash))?;
2190 let yaml = decompress_mszyml(&file.bytes)?;
2191 let document =
2192 serde_yaml::from_str::<PackageVersionDataDocument>(&yaml).context("failed to parse versionData.mszyml")?;
2193 Ok((document.versions, file.path))
2194 }
2195
2196 fn get_cached_source_file(
2197 &self,
2198 bucket: &str,
2199 source: &SourceRecord,
2200 relative_path: &str,
2201 expected_hash: Option<&str>,
2202 ) -> Result<CachedBytes> {
2203 let normalized_relative = relative_path.replace('\\', "/");
2204 let cache_path =
2205 temp_cache_path(&self.app_root, bucket, &source.identifier).join(normalized_relative.replace('/', "\\"));
2206
2207 if cache_path.exists() {
2208 let cached = fs::read(&cache_path).context("failed to read cached source file")?;
2209 if hash_matches(expected_hash, &cached) {
2210 return Ok(CachedBytes {
2211 path: cache_path,
2212 bytes: cached,
2213 });
2214 }
2215 }
2216
2217 let url = format!(
2218 "{}/{}",
2219 source.arg.trim_end_matches('/'),
2220 normalized_relative.trim_start_matches('/')
2221 );
2222 let response = self
2223 .client
2224 .get(url)
2225 .send()
2226 .context("failed to fetch source file")?
2227 .error_for_status()
2228 .context("source file request returned an error")?;
2229 let bytes = response.bytes().context("failed to read source file body")?.to_vec();
2230
2231 if let Some(hash) = expected_hash {
2232 verify_hash(hash, &bytes)?;
2233 }
2234
2235 if let Some(parent) = cache_path.parent() {
2236 fs::create_dir_all(parent).context("failed to create cache directory")?;
2237 }
2238 fs::write(&cache_path, &bytes).context("failed to write cache file")?;
2239
2240 Ok(CachedBytes {
2241 path: cache_path,
2242 bytes,
2243 })
2244 }
2245
2246 fn get_or_fetch_rest_manifest(
2247 &mut self,
2248 source: &SourceRecord,
2249 package_id: &str,
2250 version: &str,
2251 channel: &str,
2252 ) -> Result<(Vec<u8>, PathBuf)> {
2253 let cache_path = rest_manifest_cache_path_with_root(&self.app_root, source, package_id, version, channel);
2254 if cache_path.exists() {
2255 return Ok((
2256 fs::read(&cache_path).context("failed to read cached REST manifest")?,
2257 cache_path,
2258 ));
2259 }
2260
2261 let info = self.load_rest_information_by_name(&source.name)?;
2262 let contract = choose_contract(&info.server_supported_versions)
2263 .ok_or_else(|| anyhow!("no compatible REST contract for {}", source.name))?;
2264 let url = format!("{}/packageManifests/{}", source.arg.trim_end_matches('/'), package_id);
2265
2266 let mut params = vec![("Version", version.to_owned())];
2267 if !channel.is_empty() {
2268 params.push(("Channel", channel.to_owned()));
2269 }
2270 if info
2271 .required_query_parameters
2272 .iter()
2273 .any(|value| value.eq_ignore_ascii_case("Market"))
2274 {
2275 params.push(("Market", default_market()));
2276 }
2277
2278 let response = self
2279 .client
2280 .get(url)
2281 .header("Version", contract)
2282 .query(¶ms)
2283 .send()
2284 .context("REST manifest request failed")?
2285 .error_for_status()
2286 .context("REST manifest request returned an error")?;
2287 let bytes = response
2288 .bytes()
2289 .context("failed to read REST manifest response")?
2290 .to_vec();
2291
2292 if let Some(parent) = cache_path.parent() {
2293 fs::create_dir_all(parent).context("failed to create REST manifest cache directory")?;
2294 }
2295 fs::write(&cache_path, &bytes).context("failed to write REST manifest cache")?;
2296
2297 Ok((bytes, cache_path))
2298 }
2299
2300 fn load_rest_information_by_name(&mut self, name: &str) -> Result<RestInformation> {
2301 let index = self
2302 .store
2303 .sources
2304 .iter()
2305 .position(|source| source.name.eq_ignore_ascii_case(name))
2306 .ok_or_else(|| anyhow!("source '{name}' was not configured"))?;
2307 self.load_rest_information(index)
2308 }
2309
2310 fn resolve_source_indexes(&self, source_name: Option<&str>) -> Result<Vec<usize>> {
2311 if let Some(name) = source_name {
2312 let index = self
2313 .store
2314 .sources
2315 .iter()
2316 .position(|source| source.name.eq_ignore_ascii_case(name))
2317 .ok_or_else(|| anyhow!("source '{name}' was not configured"))?;
2318 return Ok(vec![index]);
2319 }
2320
2321 let mut indexes: Vec<_> = self
2322 .store
2323 .sources
2324 .iter()
2325 .enumerate()
2326 .filter(|(_, source)| !source.explicit)
2327 .collect();
2328 indexes.sort_by(|(left_index, left_source), (right_index, right_source)| {
2329 right_source
2330 .priority
2331 .cmp(&left_source.priority)
2332 .then_with(|| left_index.cmp(right_index))
2333 });
2334 Ok(indexes.into_iter().map(|(index, _)| index).collect())
2335 }
2336
2337 fn source_clone(&self, index: usize) -> SourceRecord {
2338 self.store.sources[index].clone()
2339 }
2340
2341 fn refresh_system_winget_sources(&mut self) -> Result<()> {
2342 if self.use_system_winget_sources {
2343 self.store = load_system_winget_source_store()?;
2344 }
2345 Ok(())
2346 }
2347
2348 fn update_system_winget_sources(&mut self, source_name: Option<&str>) -> Result<Vec<SourceUpdateResult>> {
2349 self.refresh_system_winget_sources()?;
2350 let selected_names: Vec<_> = if source_name.is_none() {
2351 self.store.sources.iter().map(|source| source.name.clone()).collect()
2352 } else {
2353 self.resolve_source_indexes(source_name)?
2354 .into_iter()
2355 .map(|index| self.store.sources[index].name.clone())
2356 .collect()
2357 };
2358
2359 let detail = system_winget_update_sources(source_name)?;
2360 self.refresh_system_winget_sources()?;
2361
2362 Ok(selected_names
2363 .into_iter()
2364 .filter_map(|name| {
2365 self.store
2366 .sources
2367 .iter()
2368 .find(|source| source.name.eq_ignore_ascii_case(&name))
2369 .map(|source| SourceUpdateResult {
2370 name: source.name.clone(),
2371 kind: source.kind,
2372 detail: detail.clone(),
2373 })
2374 })
2375 .collect())
2376 }
2377}
2378
2379#[derive(Debug)]
2380struct CachedBytes {
2381 path: PathBuf,
2382 bytes: Vec<u8>,
2383}
2384
2385#[derive(Debug, Clone)]
2386struct V1SearchRow {
2387 manifest_rowid: i64,
2388 package_rowid: i64,
2389 version: String,
2390 channel: String,
2391 id: String,
2392 name: String,
2393 moniker: Option<String>,
2394 match_criteria: Option<String>,
2395}
2396
2397#[derive(Debug, Clone)]
2398struct V1VersionRow {
2399 version: String,
2400 channel: String,
2401 pathpart_id: i64,
2402 manifest_hash: Option<String>,
2403}
2404
2405#[derive(Debug, Clone)]
2406struct V2SearchRow {
2407 package_rowid: i64,
2408 package_hash: String,
2409 id: String,
2410 name: String,
2411 moniker: Option<String>,
2412 version: String,
2413 match_criteria: Option<String>,
2414}
2415
2416fn list_query_needs_available_lookup(query: &ListQuery) -> bool {
2417 query.query.is_some()
2418 || query.id.is_some()
2419 || query.name.is_some()
2420 || query.moniker.is_some()
2421 || query.tag.is_some()
2422 || query.command.is_some()
2423 || query.source.is_some()
2424}
2425
2426fn package_query_from_list_query(query: &ListQuery) -> PackageQuery {
2427 PackageQuery {
2428 query: query.query.clone(),
2429 id: query.id.clone(),
2430 name: query.name.clone(),
2431 moniker: query.moniker.clone(),
2432 tag: query.tag.clone(),
2433 command: query.command.clone(),
2434 source: query.source.clone(),
2435 count: Some(LIST_LOOKUP_MAX_RESULTS),
2436 exact: query.exact,
2437 version: None,
2438 channel: None,
2439 locale: None,
2440 installer_type: None,
2441 installer_architecture: None,
2442 platform: None,
2443 os_version: None,
2444 install_scope: query.install_scope.clone(),
2445 }
2446}
2447
2448fn allow_loose_list_correlation(query: &ListQuery) -> bool {
2449 query.query.is_some() || query.id.is_some() || query.name.is_some()
2450}
2451
2452fn installed_package_has_upgrade(package: &InstalledPackage) -> bool {
2453 package
2454 .correlated
2455 .as_ref()
2456 .and_then(|candidate| candidate.version.as_deref())
2457 .is_some_and(|version| compare_version(version, &package.installed_version) == Ordering::Greater)
2458}
2459
2460fn installed_package_has_unknown_version(package: &InstalledPackage) -> bool {
2461 package.installed_version.eq_ignore_ascii_case("Unknown")
2462}
2463
2464fn installed_package_matches_upgrade_filter(package: &InstalledPackage, query: &ListQuery) -> bool {
2465 installed_package_has_upgrade(package)
2466 || (query.include_unknown && installed_package_has_unknown_version(package) && package.correlated.is_some())
2467}
2468
2469fn find_applicable_pin<'a>(item: &ListMatch, pins: &'a [PinRecord]) -> Option<&'a PinRecord> {
2470 let mut source_specific = None;
2471 let mut source_agnostic = None;
2472
2473 for pin in pins {
2474 if !pin.package_id.eq_ignore_ascii_case(&item.id) && !pin.package_id.eq_ignore_ascii_case(&item.local_id) {
2475 continue;
2476 }
2477
2478 if pin.source_id.is_empty() {
2479 if source_agnostic.is_none() {
2480 source_agnostic = Some(pin);
2481 }
2482 } else if item
2483 .source_name
2484 .as_deref()
2485 .is_some_and(|source| pin.source_id.eq_ignore_ascii_case(source))
2486 {
2487 source_specific = Some(pin);
2488 break;
2489 }
2490 }
2491
2492 source_specific.or(source_agnostic)
2493}
2494
2495fn is_upgrade_blocked_by_pin(item: &ListMatch, pins: &[PinRecord]) -> bool {
2496 let Some(available_version) = item.available_version.as_deref() else {
2497 return false;
2498 };
2499 let Some(pin) = find_applicable_pin(item, pins) else {
2500 return false;
2501 };
2502
2503 match pin.pin_type {
2504 PinType::Blocking => true,
2505 PinType::Gating | PinType::Pinning => !version_matches_pin_pattern(available_version, &pin.version),
2506 }
2507}
2508
2509fn version_matches_pin_pattern(version: &str, pattern: &str) -> bool {
2510 if pattern.trim().is_empty() || pattern == "*" {
2511 return true;
2512 }
2513
2514 if !pattern.contains('*') {
2515 return version.eq_ignore_ascii_case(pattern);
2516 }
2517
2518 if pattern.ends_with('*') && pattern.matches('*').count() == 1 {
2519 let prefix = &pattern[..pattern.len() - 1];
2520 return version.to_ascii_lowercase().starts_with(&prefix.to_ascii_lowercase());
2521 }
2522
2523 version.eq_ignore_ascii_case(pattern)
2524}
2525
2526fn list_match_from_installed(package: InstalledPackage) -> ListMatch {
2527 let available_version = package.correlated.as_ref().and_then(|candidate| {
2528 candidate.version.as_ref().and_then(|candidate_version| {
2529 if installed_package_has_unknown_version(&package)
2530 || compare_version(candidate_version, &package.installed_version) == Ordering::Greater
2531 {
2532 Some(candidate_version.clone())
2533 } else {
2534 None
2535 }
2536 })
2537 });
2538 let source_name = package
2539 .correlated
2540 .as_ref()
2541 .map(|candidate| candidate.source_name.clone())
2542 .filter(|value| !value.is_empty());
2543 let id = package
2544 .correlated
2545 .as_ref()
2546 .map(|candidate| candidate.id.clone())
2547 .unwrap_or_else(|| package.local_id.clone());
2548
2549 ListMatch {
2550 name: package.name,
2551 id,
2552 local_id: package.local_id,
2553 installed_version: package.installed_version,
2554 available_version,
2555 source_name,
2556 publisher: package.publisher,
2557 scope: package.scope,
2558 installer_category: package.installer_category,
2559 install_location: package.install_location,
2560 package_family_names: package.package_family_names,
2561 product_codes: package.product_codes,
2562 upgrade_codes: package.upgrade_codes,
2563 }
2564}
2565
2566fn list_sort_weight(package: &InstalledPackage) -> usize {
2567 if package.local_id.starts_with("ARP\\") {
2568 0
2569 } else if package.name.contains(".SparseApp") || package.local_id.contains(".SparseApp_") {
2570 1
2571 } else {
2572 2
2573 }
2574}
2575
2576fn list_package_matches(package: &InstalledPackage, query: &ListQuery) -> bool {
2577 let correlated = package.correlated.as_ref();
2578
2579 if let Some(value) = &query.id {
2580 let local_match = matches_text(&package.local_id, value, query.exact);
2581 let correlated_match = correlated
2582 .map(|candidate| matches_text(&candidate.id, value, query.exact))
2583 .unwrap_or(false);
2584 if !local_match && !correlated_match {
2585 return false;
2586 }
2587 }
2588
2589 if let Some(value) = &query.name
2590 && !matches_text(&package.name, value, query.exact)
2591 {
2592 return false;
2593 }
2594
2595 if let Some(value) = &query.product_code
2596 && !package
2597 .product_codes
2598 .iter()
2599 .any(|code| code.eq_ignore_ascii_case(value))
2600 {
2601 return false;
2602 }
2603
2604 if let Some(value) = &query.version
2605 && !package.installed_version.eq_ignore_ascii_case(value)
2606 {
2607 return false;
2608 }
2609
2610 if let Some(value) = &query.query {
2611 let local_match =
2612 matches_text(&package.name, value, query.exact) || matches_text(&package.local_id, value, query.exact);
2613 let correlated_match = correlated
2614 .map(|candidate| {
2615 matches_text(&candidate.id, value, query.exact) || matches_text(&candidate.name, value, query.exact)
2616 })
2617 .unwrap_or(false);
2618 if !local_match && !correlated_match {
2619 return false;
2620 }
2621 }
2622
2623 if let Some(source) = &query.source
2624 && correlated
2625 .map(|candidate| !candidate.source_name.eq_ignore_ascii_case(source))
2626 .unwrap_or(true)
2627 {
2628 return false;
2629 }
2630
2631 if (query.moniker.is_some() || query.tag.is_some() || query.command.is_some()) && correlated.is_none() {
2632 return false;
2633 }
2634
2635 true
2636}
2637
2638fn correlate_installed_package(
2639 package: &InstalledPackage,
2640 candidates: &[SearchMatch],
2641 allow_loose_name_match: bool,
2642) -> Option<SearchMatch> {
2643 if package.local_id.starts_with("MSIX\\") {
2644 return None;
2645 }
2646
2647 let installed_name = normalize_correlation_name(&package.name);
2648 let candidate_names = correlation_name_candidates(&package.name);
2649
2650 candidates
2651 .iter()
2652 .filter_map(|candidate| {
2653 let candidate_name = normalize_correlation_name(&candidate.name);
2654 let score = if candidate.id.eq_ignore_ascii_case(&package.local_id) {
2655 1000
2656 } else if candidate_names.iter().any(|name| {
2657 let normalized = normalize_correlation_name(name);
2658 normalized == candidate_name
2659 }) {
2660 900
2661 } else if allow_loose_name_match && candidate_name.len() >= 6 && installed_name.contains(&candidate_name) {
2662 700
2663 } else {
2664 0
2665 };
2666
2667 (score > 0).then_some((score, candidate.clone()))
2668 })
2669 .max_by_key(|(score, _)| *score)
2670 .map(|(_, candidate)| candidate)
2671}
2672
2673fn correlation_name_candidates(name: &str) -> Vec<String> {
2674 let mut candidates = Vec::new();
2675 candidates.push(name.trim().to_owned());
2676
2677 let mut trimmed = name.trim().to_owned();
2678 if let Some(index) = trimmed.find(" (") {
2679 trimmed.truncate(index);
2680 }
2681
2682 let mut words = Vec::new();
2683 for token in trimmed.split_whitespace() {
2684 let lower = token
2685 .trim_matches(|ch: char| ch == '(' || ch == ')')
2686 .to_ascii_lowercase();
2687 if !words.is_empty()
2688 && (lower == "x64" || lower == "x86" || lower == "arm64" || token.chars().any(|ch| ch.is_ascii_digit()))
2689 {
2690 break;
2691 }
2692 words.push(token);
2693 }
2694
2695 if !words.is_empty() {
2696 candidates.push(words.join(" "));
2697 }
2698
2699 candidates.sort();
2700 candidates.dedup();
2701 candidates
2702}
2703
2704fn normalize_correlation_name(value: &str) -> String {
2705 value
2706 .chars()
2707 .filter(|ch| ch.is_ascii_alphanumeric())
2708 .flat_map(|ch| ch.to_lowercase())
2709 .collect()
2710}
2711
2712#[cfg(windows)]
2713fn collect_installed_packages(scope: Option<&str>) -> Result<Vec<InstalledPackage>> {
2714 let mut packages = Vec::new();
2715 let mut seen = BTreeSet::new();
2716
2717 let machine = !matches!(scope, Some(value) if value.eq_ignore_ascii_case("user"));
2718 let user = !matches!(scope, Some(value) if value.eq_ignore_ascii_case("machine"));
2719
2720 if machine {
2721 collect_uninstall_view(
2722 &mut packages,
2723 &mut seen,
2724 RegKey::predef(HKEY_LOCAL_MACHINE),
2725 "Machine",
2726 "X64",
2727 KEY_READ | KEY_WOW64_64KEY,
2728 )?;
2729 collect_uninstall_view(
2730 &mut packages,
2731 &mut seen,
2732 RegKey::predef(HKEY_LOCAL_MACHINE),
2733 "Machine",
2734 "X86",
2735 KEY_READ | KEY_WOW64_32KEY,
2736 )?;
2737 collect_appmodel_packages(
2738 &mut packages,
2739 &mut seen,
2740 RegKey::predef(HKEY_LOCAL_MACHINE),
2741 "Machine",
2742 KEY_READ | KEY_WOW64_64KEY,
2743 )?;
2744 }
2745
2746 if user {
2747 collect_uninstall_view(
2748 &mut packages,
2749 &mut seen,
2750 RegKey::predef(HKEY_CURRENT_USER),
2751 "User",
2752 "X64",
2753 KEY_READ,
2754 )?;
2755 collect_appmodel_packages(
2756 &mut packages,
2757 &mut seen,
2758 RegKey::predef(HKEY_CURRENT_USER),
2759 "User",
2760 KEY_READ,
2761 )?;
2762 }
2763
2764 Ok(packages)
2765}
2766
2767#[cfg(not(windows))]
2768fn collect_installed_packages(_scope: Option<&str>) -> Result<Vec<InstalledPackage>> {
2769 Ok(Vec::new())
2770}
2771
2772#[cfg(windows)]
2773fn collect_uninstall_view(
2774 packages: &mut Vec<InstalledPackage>,
2775 seen: &mut BTreeSet<String>,
2776 root: RegKey,
2777 scope: &str,
2778 arch: &str,
2779 flags: u32,
2780) -> Result<()> {
2781 const UNINSTALL_PATH: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
2782
2783 let uninstall = match root.open_subkey_with_flags(UNINSTALL_PATH, flags) {
2784 Ok(key) => key,
2785 Err(_) => return Ok(()),
2786 };
2787
2788 for key_name in uninstall.enum_keys().flatten() {
2789 let subkey = match uninstall.open_subkey_with_flags(&key_name, flags) {
2790 Ok(key) => key,
2791 Err(_) => continue,
2792 };
2793 if read_reg_dword(&subkey, "SystemComponent") == Some(1) || read_reg_string(&subkey, "ParentKeyName").is_some()
2794 {
2795 continue;
2796 }
2797
2798 let Some(name) = read_reg_string(&subkey, "DisplayName").filter(|value| !value.is_empty()) else {
2799 continue;
2800 };
2801
2802 let local_id = format!(r"ARP\{scope}\{arch}\{key_name}");
2803 let installed_version = read_reg_string(&subkey, "DisplayVersion").unwrap_or_else(|| "Unknown".to_owned());
2804 let publisher = read_reg_string(&subkey, "Publisher");
2805 let install_location = read_reg_string(&subkey, "InstallLocation");
2806 let package_family_names = read_reg_string(&subkey, "PackageFamilyName")
2807 .into_iter()
2808 .collect::<Vec<_>>();
2809 let mut product_codes = read_reg_string(&subkey, "ProductCode").into_iter().collect::<Vec<_>>();
2810 if product_codes.is_empty() && looks_like_product_code(&key_name) {
2811 product_codes.push(key_name.to_ascii_lowercase());
2812 }
2813 let upgrade_codes = read_reg_string(&subkey, "UpgradeCode").into_iter().collect::<Vec<_>>();
2814 let installer_category =
2815 if local_id.starts_with("ARP\\") && read_reg_dword(&subkey, "WindowsInstaller") == Some(1) {
2816 Some("msi".to_owned())
2817 } else if key_name.starts_with("MSIX\\") {
2818 Some("msix".to_owned())
2819 } else {
2820 Some("exe".to_owned())
2821 };
2822
2823 let dedupe_key = format!(
2824 "{}|{}|{}|{}",
2825 local_id,
2826 name.to_ascii_lowercase(),
2827 installed_version.to_ascii_lowercase(),
2828 publisher.clone().unwrap_or_default().to_ascii_lowercase()
2829 );
2830 if !seen.insert(dedupe_key) {
2831 continue;
2832 }
2833
2834 packages.push(InstalledPackage {
2835 name,
2836 local_id,
2837 installed_version,
2838 publisher,
2839 scope: Some(scope.to_owned()),
2840 installer_category,
2841 install_location,
2842 package_family_names,
2843 product_codes,
2844 upgrade_codes,
2845 correlated: None,
2846 });
2847 }
2848
2849 Ok(())
2850}
2851
2852#[cfg(windows)]
2853fn collect_appmodel_packages(
2854 packages: &mut Vec<InstalledPackage>,
2855 seen: &mut BTreeSet<String>,
2856 root: RegKey,
2857 scope: &str,
2858 flags: u32,
2859) -> Result<()> {
2860 const APPMODEL_PACKAGES_PATH: &str =
2861 r"Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages";
2862
2863 let appmodel = match root.open_subkey_with_flags(APPMODEL_PACKAGES_PATH, flags) {
2864 Ok(key) => key,
2865 Err(_) => return Ok(()),
2866 };
2867
2868 for key_name in appmodel.enum_keys().flatten() {
2869 let subkey = match appmodel.open_subkey_with_flags(&key_name, flags) {
2870 Ok(key) => key,
2871 Err(_) => continue,
2872 };
2873
2874 let Some(name) = read_reg_string(&subkey, "DisplayName").filter(|value| !value.is_empty()) else {
2875 continue;
2876 };
2877 let install_location = read_reg_string(&subkey, "PackageRootFolder");
2878 if install_location.as_deref().is_some_and(is_windows_system_path) {
2879 continue;
2880 }
2881
2882 let Some(metadata) = parse_msix_package_full_name(&key_name) else {
2883 continue;
2884 };
2885
2886 let local_id = format!(r"MSIX\{key_name}");
2887 let dedupe_key = format!(
2888 "{}|{}|{}",
2889 local_id,
2890 name.to_ascii_lowercase(),
2891 metadata.version.to_ascii_lowercase()
2892 );
2893 if !seen.insert(dedupe_key) {
2894 continue;
2895 }
2896
2897 packages.push(InstalledPackage {
2898 name,
2899 local_id,
2900 installed_version: metadata.version,
2901 publisher: None,
2902 scope: Some(scope.to_owned()),
2903 installer_category: Some("msix".to_owned()),
2904 install_location,
2905 package_family_names: vec![metadata.family_name],
2906 product_codes: Vec::new(),
2907 upgrade_codes: Vec::new(),
2908 correlated: None,
2909 });
2910 }
2911
2912 Ok(())
2913}
2914
2915#[cfg(windows)]
2916struct ParsedMsixPackageFullName {
2917 version: String,
2918 family_name: String,
2919}
2920
2921#[cfg(windows)]
2922fn parse_msix_package_full_name(value: &str) -> Option<ParsedMsixPackageFullName> {
2923 let segments = value.split('_').collect::<Vec<_>>();
2924 if segments.len() < 5 {
2925 return None;
2926 }
2927
2928 let name = segments[..segments.len() - 4].join("_");
2929 if name.is_empty() {
2930 return None;
2931 }
2932
2933 let version = segments[segments.len() - 4].trim();
2934 let resource_id = segments[segments.len() - 2].trim();
2935 let publisher_id = segments[segments.len() - 1].trim();
2936 if version.is_empty() || publisher_id.is_empty() {
2937 return None;
2938 }
2939
2940 let family_name = if resource_id.is_empty() {
2941 format!("{name}_{publisher_id}")
2942 } else {
2943 format!("{name}_{resource_id}_{publisher_id}")
2944 };
2945
2946 Some(ParsedMsixPackageFullName {
2947 version: version.to_owned(),
2948 family_name,
2949 })
2950}
2951
2952#[cfg(windows)]
2953fn is_windows_system_path(path: &str) -> bool {
2954 path.trim().to_ascii_lowercase().starts_with(r"c:\windows\")
2955}
2956
2957#[cfg(windows)]
2958fn read_reg_string(key: &RegKey, value_name: &str) -> Option<String> {
2959 key.get_value::<String, _>(value_name)
2960 .ok()
2961 .map(|value| value.trim().to_owned())
2962 .filter(|value| !value.is_empty())
2963}
2964
2965#[cfg(windows)]
2966fn read_reg_dword(key: &RegKey, value_name: &str) -> Option<u32> {
2967 key.get_value::<u32, _>(value_name).ok()
2968}
2969
2970#[cfg(windows)]
2971fn looks_like_product_code(value: &str) -> bool {
2972 value.starts_with('{') && value.ends_with('}')
2973}
2974
2975fn ensure_app_dirs(app_root: &Path) -> Result<()> {
2976 fs::create_dir_all(app_root).context("failed to create app root directory")?;
2977 if uses_packaged_layout(app_root) {
2978 fs::create_dir_all(packaged_file_cache_root(app_root)).context("failed to create packaged file cache root")?;
2979 let user_sources_path = packaged_user_sources_path(app_root)?;
2980 if let Some(parent) = user_sources_path.parent() {
2981 fs::create_dir_all(parent).context("failed to create packaged secure settings directory")?;
2982 }
2983 } else {
2984 fs::create_dir_all(app_root.join("sources")).context("failed to create app source directory")?;
2985 }
2986 Ok(())
2987}
2988
2989fn default_app_root() -> Result<PathBuf> {
2990 if let Some(app_root) = std::env::var_os("PINGET_APPROOT") {
2991 return Ok(PathBuf::from(app_root));
2992 }
2993
2994 let local_app_data = dirs::data_local_dir().ok_or_else(|| anyhow!("unable to determine LocalAppData path"))?;
2995
2996 #[cfg(windows)]
2997 {
2998 Ok(local_app_data
2999 .join("Packages")
3000 .join(PACKAGED_FAMILY_NAME)
3001 .join("LocalState"))
3002 }
3003
3004 #[cfg(not(windows))]
3005 {
3006 Ok(local_app_data.join("pinget"))
3007 }
3008}
3009
3010fn store_path(app_root: &Path) -> PathBuf {
3011 app_root.join("sources.json")
3012}
3013
3014fn user_settings_path(app_root: &Path) -> PathBuf {
3015 if uses_packaged_layout(app_root) {
3016 app_root.join("settings.json")
3017 } else {
3018 app_root.join("user-settings.json")
3019 }
3020}
3021
3022fn admin_settings_path(app_root: &Path) -> PathBuf {
3023 app_root.join("admin-settings.json")
3024}
3025
3026fn source_state_dir(app_root: &Path, source: &SourceRecord) -> PathBuf {
3027 if uses_packaged_layout(app_root) {
3028 return app_root
3029 .join(packaged_source_type(source.kind))
3030 .join(sanitize_path_segment(&source.identifier));
3031 }
3032
3033 app_root.join("sources").join(sanitize_path_segment(&source.name))
3034}
3035
3036fn preindexed_package_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3037 source_state_dir(app_root, source).join("source.msix")
3038}
3039
3040fn preindexed_index_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3041 source_state_dir(app_root, source).join("index.db")
3042}
3043
3044fn rest_information_cache_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3045 source_state_dir(app_root, source).join("rest-information.json")
3046}
3047
3048fn rest_manifest_cache_path_with_root(
3049 app_root: &Path,
3050 source: &SourceRecord,
3051 package_id: &str,
3052 version: &str,
3053 channel: &str,
3054) -> PathBuf {
3055 let key = format!("{}|{}|{}|{}", source.identifier, package_id, version, channel);
3056 let digest = sha256_hex(key.as_bytes());
3057 cache_root(app_root)
3058 .join("REST_M")
3059 .join(sanitize_path_segment(&source.identifier))
3060 .join(format!("{digest}.json"))
3061}
3062
3063fn temp_cache_path(app_root: &Path, bucket: &str, identifier: &str) -> PathBuf {
3064 cache_root(app_root)
3065 .join(bucket)
3066 .join(sanitize_path_segment(identifier))
3067}
3068
3069fn load_store(app_root: &Path) -> Result<SourceStore> {
3070 if uses_packaged_layout(app_root)
3071 && let Some(store) = load_packaged_store(app_root)?
3072 {
3073 return Ok(store);
3074 }
3075
3076 let path = store_path(app_root);
3077 if !path.exists() {
3078 let store = SourceStore::default();
3079 save_store(app_root, &store)?;
3080 return Ok(store);
3081 }
3082
3083 let bytes = fs::read(path).context("failed to read source store")?;
3084 serde_json::from_slice(&bytes).context("failed to parse source store")
3085}
3086
3087fn save_store(app_root: &Path, store: &SourceStore) -> Result<()> {
3088 if uses_system_winget_source_commands(app_root) {
3089 bail!("System WinGet source settings must be modified through winget.");
3090 }
3091
3092 if uses_packaged_layout(app_root) {
3093 return save_packaged_store(app_root, store);
3094 }
3095
3096 write_json(store_path(app_root), store)
3097}
3098
3099fn load_packaged_store(app_root: &Path) -> Result<Option<SourceStore>> {
3100 let user_sources_path = packaged_user_sources_path(app_root)?;
3101 let metadata_path = app_root.join("sources_metadata");
3102 let user_sources_yaml = fs::read_to_string(&user_sources_path).ok();
3103 let metadata_yaml = fs::read_to_string(&metadata_path).ok();
3104 load_packaged_store_from_streams(user_sources_yaml.as_deref(), metadata_yaml.as_deref())
3105}
3106
3107fn load_packaged_store_from_streams(
3108 user_sources_yaml: Option<&str>,
3109 metadata_yaml: Option<&str>,
3110) -> Result<Option<SourceStore>> {
3111 if is_secure_settings_stub(user_sources_yaml) {
3112 return load_system_winget_source_store().map(Some);
3113 }
3114
3115 Ok(parse_packaged_source_store(user_sources_yaml, metadata_yaml))
3116}
3117
3118fn save_packaged_store(app_root: &Path, store: &SourceStore) -> Result<()> {
3119 let user_sources_path = packaged_user_sources_path(app_root)?;
3120 if let Some(parent) = user_sources_path.parent() {
3121 fs::create_dir_all(parent).context("failed to create packaged source stream directory")?;
3122 }
3123
3124 fs::write(&user_sources_path, render_packaged_sources_yaml(store))
3125 .context("failed to write packaged user_sources stream")?;
3126 fs::write(app_root.join("sources_metadata"), render_packaged_metadata_yaml(store))
3127 .context("failed to write packaged source metadata")?;
3128 Ok(())
3129}
3130
3131fn uses_system_winget_source_commands(app_root: &Path) -> bool {
3132 if !uses_packaged_layout(app_root) {
3133 return false;
3134 }
3135
3136 #[cfg(windows)]
3137 {
3138 let Ok(user_sources_path) = packaged_user_sources_path(app_root) else {
3139 return false;
3140 };
3141
3142 let user_sources_yaml = fs::read_to_string(user_sources_path).ok();
3143 is_secure_settings_stub(user_sources_yaml.as_deref())
3144 }
3145
3146 #[cfg(not(windows))]
3147 {
3148 false
3149 }
3150}
3151
3152fn is_secure_settings_stub(value: Option<&str>) -> bool {
3153 value
3154 .map(str::trim_start)
3155 .is_some_and(|value| value.starts_with("SHA256:") || value.starts_with("sha256:"))
3156}
3157
3158fn load_system_winget_source_store() -> Result<SourceStore> {
3159 let result = run_checked_winget_source_command(build_winget_source_export_arguments(), "export WinGet sources")?;
3160 Ok(SourceStore {
3161 sources: parse_system_winget_source_export(&result.stdout)?,
3162 })
3163}
3164
3165fn system_winget_add_source(name: &str, arg: &str, kind: SourceKind, trust_level: &str, explicit: bool) -> Result<()> {
3166 run_checked_winget_source_command(
3167 build_winget_source_add_arguments(name, arg, kind, trust_level, explicit),
3168 &format!("add WinGet source '{name}'"),
3169 )?;
3170 Ok(())
3171}
3172
3173fn system_winget_remove_source(name: &str) -> Result<()> {
3174 run_checked_winget_source_command(
3175 vec![
3176 "source".to_owned(),
3177 "remove".to_owned(),
3178 "--name".to_owned(),
3179 name.to_owned(),
3180 "--disable-interactivity".to_owned(),
3181 ],
3182 &format!("remove WinGet source '{name}'"),
3183 )?;
3184 Ok(())
3185}
3186
3187fn system_winget_reset_source(name: &str) -> Result<()> {
3188 run_checked_winget_source_command(
3189 vec![
3190 "source".to_owned(),
3191 "reset".to_owned(),
3192 "--name".to_owned(),
3193 name.to_owned(),
3194 "--force".to_owned(),
3195 "--disable-interactivity".to_owned(),
3196 ],
3197 &format!("reset WinGet source '{name}'"),
3198 )?;
3199 Ok(())
3200}
3201
3202fn system_winget_reset_sources() -> Result<()> {
3203 run_checked_winget_source_command(
3204 vec![
3205 "source".to_owned(),
3206 "reset".to_owned(),
3207 "--force".to_owned(),
3208 "--disable-interactivity".to_owned(),
3209 ],
3210 "reset WinGet sources",
3211 )?;
3212 Ok(())
3213}
3214
3215fn system_winget_update_sources(name: Option<&str>) -> Result<String> {
3216 let mut args = vec!["source".to_owned(), "update".to_owned()];
3217 if let Some(name) = name {
3218 args.push("--name".to_owned());
3219 args.push(name.to_owned());
3220 }
3221
3222 args.push("--disable-interactivity".to_owned());
3223 let result = run_checked_winget_source_command(
3224 args,
3225 if name.is_some() {
3226 "update WinGet source"
3227 } else {
3228 "update WinGet sources"
3229 },
3230 )?;
3231 let detail = result.stdout.trim();
3232 Ok(if detail.is_empty() {
3233 "Done".to_owned()
3234 } else {
3235 detail.to_owned()
3236 })
3237}
3238
3239fn build_winget_source_export_arguments() -> Vec<String> {
3240 vec![
3241 "source".to_owned(),
3242 "export".to_owned(),
3243 "--disable-interactivity".to_owned(),
3244 ]
3245}
3246
3247fn build_winget_source_add_arguments(
3248 name: &str,
3249 arg: &str,
3250 kind: SourceKind,
3251 trust_level: &str,
3252 explicit: bool,
3253) -> Vec<String> {
3254 let mut args = vec![
3255 "source".to_owned(),
3256 "add".to_owned(),
3257 "--name".to_owned(),
3258 name.to_owned(),
3259 "--arg".to_owned(),
3260 arg.to_owned(),
3261 "--type".to_owned(),
3262 packaged_source_type(kind).to_owned(),
3263 "--disable-interactivity".to_owned(),
3264 ];
3265
3266 if !trust_level.trim().is_empty() {
3267 args.push("--trust-level".to_owned());
3268 args.push(if trust_level.eq_ignore_ascii_case("Trusted") {
3269 "trusted".to_owned()
3270 } else {
3271 "none".to_owned()
3272 });
3273 }
3274
3275 if explicit {
3276 args.push("--explicit".to_owned());
3277 }
3278
3279 args
3280}
3281
3282#[derive(Debug)]
3283struct WingetSourceCommandResult {
3284 exit_code: i32,
3285 stdout: String,
3286 stderr: String,
3287}
3288
3289type WingetSourceCommandRunner = fn(&[String]) -> Result<WingetSourceCommandResult>;
3290
3291static SYSTEM_WINGET_SOURCE_COMMAND_RUNNER: RwLock<WingetSourceCommandRunner> =
3292 RwLock::new(run_winget_source_command_process);
3293
3294fn run_checked_winget_source_command(args: Vec<String>, action: &str) -> Result<WingetSourceCommandResult> {
3295 let result = run_winget_source_command(&args)?;
3296 if result.exit_code == 0 {
3297 return Ok(result);
3298 }
3299
3300 let detail = [result.stderr.trim(), result.stdout.trim()]
3301 .into_iter()
3302 .filter(|value| !value.is_empty())
3303 .collect::<Vec<_>>()
3304 .join("\n");
3305 if detail.is_empty() {
3306 bail!("Failed to {action}; winget exited with code {}.", result.exit_code);
3307 }
3308
3309 bail!(
3310 "Failed to {action}; winget exited with code {}:\n{}",
3311 result.exit_code,
3312 detail
3313 );
3314}
3315
3316fn run_winget_source_command(args: &[String]) -> Result<WingetSourceCommandResult> {
3317 let runner = *SYSTEM_WINGET_SOURCE_COMMAND_RUNNER
3318 .read()
3319 .map_err(|_| anyhow!("failed to read WinGet source command runner"))?;
3320 runner(args)
3321}
3322
3323fn run_winget_source_command_process(args: &[String]) -> Result<WingetSourceCommandResult> {
3324 use std::process::Command;
3325
3326 let output = Command::new("winget")
3327 .args(args)
3328 .output()
3329 .context("failed to run winget source command")?;
3330
3331 Ok(WingetSourceCommandResult {
3332 exit_code: output.status.code().unwrap_or(-1),
3333 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
3334 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
3335 })
3336}
3337
3338fn parse_system_winget_source_export(output: &str) -> Result<Vec<SourceRecord>> {
3339 let trimmed = output.trim();
3340 if trimmed.is_empty() {
3341 return Ok(Vec::new());
3342 }
3343
3344 if let Ok(value) = serde_json::from_str::<JsonValue>(trimmed)
3345 && let Some(sources) = parse_system_winget_source_export_value(&value)?
3346 {
3347 return Ok(order_sources(sources));
3348 }
3349
3350 let mut sources = Vec::new();
3351 for line in output.replace("\r\n", "\n").lines().map(str::trim) {
3352 if line.is_empty() {
3353 continue;
3354 }
3355
3356 let value: JsonValue = serde_json::from_str(line)
3357 .with_context(|| format!("WinGet source export returned a non-source line: {line}"))?;
3358 let Some(mut parsed) = parse_system_winget_source_export_value(&value)? else {
3359 bail!("WinGet source export returned a non-source line: {line}");
3360 };
3361 sources.append(&mut parsed);
3362 }
3363
3364 Ok(order_sources(sources))
3365}
3366
3367fn parse_system_winget_source_export_value(value: &JsonValue) -> Result<Option<Vec<SourceRecord>>> {
3368 match value {
3369 JsonValue::Array(items) => items
3370 .iter()
3371 .map(parse_system_winget_source_record)
3372 .collect::<Result<Vec<_>>>()
3373 .map(Some),
3374 JsonValue::Object(object) => {
3375 if let Some(JsonValue::Array(items)) = object.get("Sources") {
3376 items
3377 .iter()
3378 .map(parse_system_winget_source_record)
3379 .collect::<Result<Vec<_>>>()
3380 .map(Some)
3381 } else if object.contains_key("Name") {
3382 Ok(Some(vec![parse_system_winget_source_record(value)?]))
3383 } else {
3384 Ok(None)
3385 }
3386 }
3387 _ => Ok(None),
3388 }
3389}
3390
3391fn parse_system_winget_source_record(value: &JsonValue) -> Result<SourceRecord> {
3392 let object = value
3393 .as_object()
3394 .ok_or_else(|| anyhow!("WinGet source export entry was not an object"))?;
3395 let name = required_json_string(object, "Name")?;
3396 let source_type = required_json_string(object, "Type")?;
3397 let arg = required_json_string(object, "Arg")?;
3398 let kind = parse_system_winget_source_kind(&source_type)?;
3399 let identifier = optional_json_string(object, "Identifier")
3400 .or_else(|| optional_json_string(object, "Data"))
3401 .unwrap_or_else(|| name.clone());
3402
3403 Ok(SourceRecord {
3404 name,
3405 kind,
3406 arg,
3407 identifier,
3408 trust_level: parse_system_winget_trust_level(object),
3409 explicit: optional_json_bool(object, "Explicit").unwrap_or(false),
3410 priority: optional_json_i32(object, "Priority").unwrap_or_default(),
3411 last_update: None,
3412 source_version: None,
3413 })
3414}
3415
3416fn required_json_string(object: &JsonMap<String, JsonValue>, key: &str) -> Result<String> {
3417 optional_json_string(object, key).ok_or_else(|| anyhow!("WinGet source export did not include '{key}'."))
3418}
3419
3420fn optional_json_string(object: &JsonMap<String, JsonValue>, key: &str) -> Option<String> {
3421 match object.get(key)? {
3422 JsonValue::String(value) => Some(value.clone()),
3423 JsonValue::Number(value) => Some(value.to_string()),
3424 _ => None,
3425 }
3426}
3427
3428fn optional_json_bool(object: &JsonMap<String, JsonValue>, key: &str) -> Option<bool> {
3429 match object.get(key)? {
3430 JsonValue::Bool(value) => Some(*value),
3431 JsonValue::String(value) => value.parse().ok(),
3432 _ => None,
3433 }
3434}
3435
3436fn optional_json_i32(object: &JsonMap<String, JsonValue>, key: &str) -> Option<i32> {
3437 match object.get(key)? {
3438 JsonValue::Number(value) => value.as_i64().and_then(|value| i32::try_from(value).ok()),
3439 JsonValue::String(value) => value.parse().ok(),
3440 _ => None,
3441 }
3442}
3443
3444fn parse_system_winget_source_kind(value: &str) -> Result<SourceKind> {
3445 if value.eq_ignore_ascii_case("Microsoft.PreIndexed.Package") {
3446 Ok(SourceKind::PreIndexed)
3447 } else if value.eq_ignore_ascii_case("Microsoft.Rest") {
3448 Ok(SourceKind::Rest)
3449 } else {
3450 bail!("Unsupported WinGet source type '{value}'.")
3451 }
3452}
3453
3454fn parse_system_winget_trust_level(object: &JsonMap<String, JsonValue>) -> String {
3455 let Some(value) = object.get("TrustLevel") else {
3456 return "None".to_owned();
3457 };
3458
3459 match value {
3460 JsonValue::Array(items) if items.iter().any(json_trust_value_is_trusted) => "Trusted".to_owned(),
3461 JsonValue::String(_) if json_trust_value_is_trusted(value) => "Trusted".to_owned(),
3462 JsonValue::Number(value) if value.as_i64().unwrap_or_default() > 0 => "Trusted".to_owned(),
3463 _ => "None".to_owned(),
3464 }
3465}
3466
3467fn json_trust_value_is_trusted(value: &JsonValue) -> bool {
3468 value
3469 .as_str()
3470 .is_some_and(|value| value.eq_ignore_ascii_case("Trusted"))
3471}
3472
3473fn order_sources(mut sources: Vec<SourceRecord>) -> Vec<SourceRecord> {
3474 sources.sort_by(|left, right| {
3475 left.explicit
3476 .cmp(&right.explicit)
3477 .then_with(|| right.priority.cmp(&left.priority))
3478 .then_with(|| left.name.to_ascii_lowercase().cmp(&right.name.to_ascii_lowercase()))
3479 });
3480 sources
3481}
3482
3483fn parse_packaged_source_store(user_sources_yaml: Option<&str>, metadata_yaml: Option<&str>) -> Option<SourceStore> {
3484 if user_sources_yaml.is_none() && metadata_yaml.is_none() {
3485 return None;
3486 }
3487
3488 let mut sources = SourceStore::default().sources;
3489 for entry in parse_packaged_yaml_entries(user_sources_yaml) {
3490 let Some(name) = entry.get("Name").and_then(|value| value.clone()) else {
3491 continue;
3492 };
3493
3494 if yaml_bool(entry.get("IsTombstone")) {
3495 sources.retain(|source| !source.name.eq_ignore_ascii_case(&name));
3496 continue;
3497 }
3498
3499 let Some(mapped) = map_packaged_source_entry(&entry) else {
3500 continue;
3501 };
3502
3503 if let Some(existing) = sources
3504 .iter_mut()
3505 .find(|source| source.name.eq_ignore_ascii_case(&name))
3506 {
3507 *existing = mapped;
3508 } else {
3509 sources.push(mapped);
3510 }
3511 }
3512
3513 for entry in parse_packaged_yaml_entries(metadata_yaml) {
3514 let Some(name) = entry.get("Name").and_then(|value| value.clone()) else {
3515 continue;
3516 };
3517
3518 let Some(source) = sources
3519 .iter_mut()
3520 .find(|source| source.name.eq_ignore_ascii_case(&name))
3521 else {
3522 continue;
3523 };
3524
3525 if let Some(last_update) = entry
3526 .get("LastUpdate")
3527 .and_then(|value| value.as_deref())
3528 .and_then(parse_packaged_datetime)
3529 {
3530 source.last_update = Some(last_update);
3531 }
3532
3533 if let Some(source_version) = entry.get("SourceVersion").and_then(|value| value.clone())
3534 && !source_version.is_empty()
3535 {
3536 source.source_version = Some(source_version);
3537 }
3538 }
3539
3540 Some(SourceStore { sources })
3541}
3542
3543fn parse_packaged_yaml_entries(yaml: Option<&str>) -> Vec<BTreeMap<String, Option<String>>> {
3544 let Some(yaml) = yaml else {
3545 return Vec::new();
3546 };
3547
3548 let mut entries = Vec::new();
3549 let mut current: Option<BTreeMap<String, Option<String>>> = None;
3550 for raw_line in yaml.replace("\r\n", "\n").split('\n') {
3551 let line = raw_line.trim_end();
3552 let trimmed = line.trim();
3553 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.eq_ignore_ascii_case("Sources:") {
3554 continue;
3555 }
3556
3557 if let Some(remainder) = trimmed.strip_prefix("- ") {
3558 if let Some(entry) = current.take() {
3559 entries.push(entry);
3560 }
3561
3562 let mut entry = BTreeMap::new();
3563 add_packaged_yaml_key_value(&mut entry, remainder);
3564 current = Some(entry);
3565 continue;
3566 }
3567
3568 if let Some(entry) = current.as_mut() {
3569 add_packaged_yaml_key_value(entry, trimmed);
3570 }
3571 }
3572
3573 if let Some(entry) = current {
3574 entries.push(entry);
3575 }
3576
3577 entries
3578}
3579
3580fn add_packaged_yaml_key_value(entry: &mut BTreeMap<String, Option<String>>, line: &str) {
3581 let Some((key, value)) = line.split_once(':') else {
3582 return;
3583 };
3584
3585 entry.insert(
3586 key.trim().to_owned(),
3587 Some(unquote_yaml_scalar(value.trim()).to_owned()),
3588 );
3589}
3590
3591fn unquote_yaml_scalar(value: &str) -> &str {
3592 if value.len() >= 2 {
3593 let bytes = value.as_bytes();
3594 let first = bytes[0] as char;
3595 let last = bytes[value.len() - 1] as char;
3596 if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
3597 return &value[1..value.len() - 1];
3598 }
3599 }
3600
3601 value
3602}
3603
3604fn map_packaged_source_entry(entry: &BTreeMap<String, Option<String>>) -> Option<SourceRecord> {
3605 let name = entry.get("Name").and_then(|value| value.clone())?;
3606 let source_type = entry.get("Type").and_then(|value| value.clone())?;
3607 let arg = entry.get("Arg").and_then(|value| value.clone())?;
3608 let kind = parse_packaged_source_kind(&source_type)?;
3609
3610 Some(SourceRecord {
3611 name: name.clone(),
3612 kind,
3613 arg,
3614 identifier: entry
3615 .get("Identifier")
3616 .and_then(|value| value.clone())
3617 .or_else(|| entry.get("Data").and_then(|value| value.clone()))
3618 .unwrap_or(name),
3619 trust_level: parse_packaged_trust_level(entry.get("TrustLevel").and_then(|value| value.as_deref())),
3620 explicit: yaml_bool(entry.get("Explicit")),
3621 priority: entry
3622 .get("Priority")
3623 .and_then(|value| value.as_deref())
3624 .and_then(|value| value.parse::<i32>().ok())
3625 .unwrap_or_default(),
3626 last_update: None,
3627 source_version: None,
3628 })
3629}
3630
3631fn parse_packaged_source_kind(value: &str) -> Option<SourceKind> {
3632 if value.eq_ignore_ascii_case("Microsoft.PreIndexed.Package") {
3633 Some(SourceKind::PreIndexed)
3634 } else if value.eq_ignore_ascii_case("Microsoft.Rest") {
3635 Some(SourceKind::Rest)
3636 } else {
3637 None
3638 }
3639}
3640
3641fn parse_packaged_trust_level(value: Option<&str>) -> String {
3642 match value {
3643 Some(raw) if raw.eq_ignore_ascii_case("Trusted") => "Trusted".to_owned(),
3644 Some(raw) if raw.parse::<i64>().ok().unwrap_or_default() > 0 => "Trusted".to_owned(),
3645 _ => "None".to_owned(),
3646 }
3647}
3648
3649fn yaml_bool(value: Option<&Option<String>>) -> bool {
3650 value
3651 .and_then(|raw| raw.as_deref())
3652 .and_then(|raw| raw.parse::<bool>().ok())
3653 .unwrap_or(false)
3654}
3655
3656fn parse_packaged_datetime(value: &str) -> Option<DateTime<Utc>> {
3657 if let Ok(unix_seconds) = value.parse::<i64>() {
3658 return DateTime::<Utc>::from_timestamp(unix_seconds, 0);
3659 }
3660
3661 DateTime::parse_from_rfc3339(value)
3662 .ok()
3663 .map(|value| value.with_timezone(&Utc))
3664}
3665
3666fn render_packaged_sources_yaml(store: &SourceStore) -> String {
3667 let mut lines = vec!["Sources:".to_owned()];
3668 for source in &store.sources {
3669 lines.push(format!(" - Name: {}", source.name));
3670 lines.push(format!(" Type: {}", packaged_source_type(source.kind)));
3671 lines.push(format!(" Arg: {}", yaml_scalar(&source.arg)));
3672 lines.push(format!(" Data: {}", yaml_scalar(&source.identifier)));
3673 lines.push(format!(
3674 " TrustLevel: {}",
3675 if source.trust_level.eq_ignore_ascii_case("Trusted") {
3676 1
3677 } else {
3678 0
3679 }
3680 ));
3681 lines.push(format!(" Explicit: {}", source.explicit));
3682 lines.push(format!(" Priority: {}", source.priority));
3683 lines.push(" IsTombstone: false".to_owned());
3684 }
3685 lines.push(String::new());
3686 lines.join("\n")
3687}
3688
3689fn render_packaged_metadata_yaml(store: &SourceStore) -> String {
3690 let mut lines = vec!["Sources:".to_owned()];
3691 for source in &store.sources {
3692 if source.last_update.is_none() && source.source_version.is_none() {
3693 continue;
3694 }
3695
3696 lines.push(format!(" - Name: {}", source.name));
3697 if let Some(last_update) = source.last_update {
3698 lines.push(format!(" LastUpdate: {}", last_update.timestamp()));
3699 }
3700 if let Some(source_version) = &source.source_version {
3701 lines.push(format!(" SourceVersion: {}", yaml_scalar(source_version)));
3702 }
3703 }
3704 lines.push(String::new());
3705 lines.join("\n")
3706}
3707
3708fn yaml_scalar(value: &str) -> String {
3709 if value.chars().any(|ch| ch.is_whitespace() || ch == ':') {
3710 format!("\"{}\"", value.replace('"', "\\\""))
3711 } else {
3712 value.to_owned()
3713 }
3714}
3715
3716fn uses_packaged_layout(app_root: &Path) -> bool {
3717 #[cfg(windows)]
3718 {
3719 try_get_packaged_local_state_root(app_root).is_some()
3720 }
3721
3722 #[cfg(not(windows))]
3723 {
3724 let _ = app_root;
3725 false
3726 }
3727}
3728
3729#[cfg(windows)]
3730fn try_get_packaged_local_state_root(app_root: &Path) -> Option<PathBuf> {
3731 let local_state = app_root.file_name()?.to_string_lossy();
3732 if !local_state.eq_ignore_ascii_case("LocalState") {
3733 return None;
3734 }
3735
3736 let family = app_root.parent()?.file_name()?.to_string_lossy();
3737 if !family.eq_ignore_ascii_case(PACKAGED_FAMILY_NAME) {
3738 return None;
3739 }
3740
3741 let packages = app_root.parent()?.parent()?.file_name()?.to_string_lossy();
3742 if !packages.eq_ignore_ascii_case("Packages") {
3743 return None;
3744 }
3745
3746 Some(app_root.to_path_buf())
3747}
3748
3749#[cfg(windows)]
3750fn packaged_user_sources_path(app_root: &Path) -> Result<PathBuf> {
3751 let _ = try_get_packaged_local_state_root(app_root).ok_or_else(|| anyhow!("not a packaged WinGet app root"))?;
3752 Ok(packaged_secure_settings_root()?.join("user_sources"))
3753}
3754
3755#[cfg(not(windows))]
3756fn packaged_user_sources_path(_app_root: &Path) -> Result<PathBuf> {
3757 bail!("packaged WinGet source streams are only available on Windows")
3758}
3759
3760#[cfg(windows)]
3761fn packaged_secure_settings_root() -> Result<PathBuf> {
3762 let program_data = std::env::var_os("PROGRAMDATA")
3763 .map(PathBuf::from)
3764 .ok_or_else(|| anyhow!("unable to determine ProgramData path"))?;
3765 Ok(program_data
3766 .join("Microsoft")
3767 .join("WinGet")
3768 .join(current_user_sid()?)
3769 .join("settings")
3770 .join("pkg")
3771 .join(PACKAGED_NAME))
3772}
3773
3774#[cfg(windows)]
3775#[allow(clippy::multiple_unsafe_ops_per_block)]
3776fn current_user_sid() -> Result<String> {
3777 fn close_token(token: HANDLE) {
3778 if !token.is_null() {
3779 unsafe {
3781 CloseHandle(token);
3782 }
3783 }
3784 }
3785
3786 let mut token: HANDLE = std::ptr::null_mut();
3787 let current_process = unsafe { GetCurrentProcess() };
3789 if unsafe { OpenProcessToken(current_process, TOKEN_QUERY, &mut token) } == 0 {
3791 bail!("failed to open current process token")
3792 }
3793
3794 let mut length = 0;
3795 unsafe {
3797 GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut length);
3798 }
3799
3800 let element_size = size_of::<TOKEN_USER>();
3801 let element_count = (length as usize).div_ceil(element_size);
3802 let mut buffer = vec![MaybeUninit::<TOKEN_USER>::uninit(); element_count];
3803 if unsafe { GetTokenInformation(token, TokenUser, buffer.as_mut_ptr().cast(), length, &mut length) } == 0 {
3805 close_token(token);
3806 bail!("failed to get current user token information")
3807 }
3808
3809 let token_user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
3811 let mut sid_ptr: *mut u16 = std::ptr::null_mut();
3812 if unsafe { ConvertSidToStringSidW(token_user.User.Sid, &mut sid_ptr) } == 0 {
3814 close_token(token);
3815 bail!("failed to convert current user SID")
3816 }
3817
3818 let mut len = 0usize;
3819 while unsafe { *sid_ptr.wrapping_add(len) } != 0 {
3821 len += 1;
3822 }
3823
3824 let sid_slice = unsafe { std::slice::from_raw_parts(sid_ptr, len) };
3826 let sid = std::ffi::OsString::from_wide(sid_slice).to_string_lossy().into_owned();
3827 unsafe {
3829 LocalFree(sid_ptr.cast());
3830 }
3831 close_token(token);
3832 Ok(sid)
3833}
3834
3835fn packaged_file_cache_root(app_root: &Path) -> PathBuf {
3836 app_root.join("Microsoft").join("Windows Package Manager")
3837}
3838
3839fn cache_root(app_root: &Path) -> PathBuf {
3840 if uses_packaged_layout(app_root) {
3841 packaged_file_cache_root(app_root)
3842 } else {
3843 default_cache_root_fallback()
3844 }
3845}
3846
3847fn default_cache_root_fallback() -> PathBuf {
3848 std::env::temp_dir().join("cache")
3849}
3850
3851fn packaged_source_type(kind: SourceKind) -> &'static str {
3852 match kind {
3853 SourceKind::PreIndexed => "Microsoft.PreIndexed.Package",
3854 SourceKind::Rest => "Microsoft.Rest",
3855 }
3856}
3857
3858fn sanitize_path_segment(value: &str) -> String {
3859 value
3860 .chars()
3861 .map(|ch| match ch {
3862 '\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
3863 _ => ch,
3864 })
3865 .collect()
3866}
3867
3868fn load_json_object(path: &Path) -> Result<JsonMap<String, JsonValue>> {
3869 if !path.exists() {
3870 return Ok(JsonMap::new());
3871 }
3872
3873 let bytes = fs::read(path).context("failed to read JSON file")?;
3874 let value: JsonValue = serde_json::from_slice(&bytes).context("failed to parse JSON file")?;
3875 Ok(match value {
3876 JsonValue::Object(object) => object,
3877 _ => JsonMap::new(),
3878 })
3879}
3880
3881fn save_json_object(path: PathBuf, value: &JsonMap<String, JsonValue>) -> Result<()> {
3882 write_json(path, value)
3883}
3884
3885fn write_json<T: Serialize>(path: PathBuf, value: &T) -> Result<()> {
3886 if let Some(parent) = path.parent() {
3887 fs::create_dir_all(parent).context("failed to create JSON parent directory")?;
3888 }
3889
3890 let bytes = serde_json::to_vec_pretty(value).context("failed to serialize JSON")?;
3891 fs::write(path, bytes).context("failed to write JSON file")
3892}
3893
3894fn merge_json_objects(
3895 current: &JsonMap<String, JsonValue>,
3896 update: &JsonMap<String, JsonValue>,
3897) -> JsonMap<String, JsonValue> {
3898 let mut merged = current.clone();
3899 for (key, value) in update {
3900 match (merged.get(key), value) {
3901 (Some(JsonValue::Object(current_object)), JsonValue::Object(update_object)) => {
3902 merged.insert(
3903 key.clone(),
3904 JsonValue::Object(merge_json_objects(current_object, update_object)),
3905 );
3906 }
3907 _ => {
3908 merged.insert(key.clone(), value.clone());
3909 }
3910 }
3911 }
3912
3913 merged
3914}
3915
3916fn json_contains(current: &JsonValue, expected: &JsonValue) -> bool {
3917 match expected {
3918 JsonValue::Object(expected_object) => match current {
3919 JsonValue::Object(current_object) => expected_object.iter().all(|(key, expected_child)| {
3920 current_object
3921 .get(key)
3922 .is_some_and(|current_child| json_contains(current_child, expected_child))
3923 }),
3924 _ => false,
3925 },
3926 JsonValue::Array(expected_array) => match current {
3927 JsonValue::Array(current_array) => {
3928 current_array.len() == expected_array.len()
3929 && current_array
3930 .iter()
3931 .zip(expected_array.iter())
3932 .all(|(current_child, expected_child)| json_contains(current_child, expected_child))
3933 }
3934 _ => false,
3935 },
3936 _ => current == expected,
3937 }
3938}
3939
3940fn normalize_source_trust_level(trust_level: Option<&str>) -> Result<String> {
3941 match trust_level.unwrap_or("none").trim().to_ascii_lowercase().as_str() {
3942 "none" | "default" => Ok("None".to_owned()),
3943 "trusted" => Ok("Trusted".to_owned()),
3944 other => bail!("unsupported source trust level: {other}"),
3945 }
3946}
3947
3948fn normalize_admin_setting_name(name: &str) -> Result<&'static str> {
3949 SUPPORTED_ADMIN_SETTINGS
3950 .iter()
3951 .copied()
3952 .find(|setting| setting.eq_ignore_ascii_case(name))
3953 .ok_or_else(|| anyhow!("unsupported admin setting: {name}"))
3954}
3955
3956fn default_source_trust_level() -> String {
3957 "None".to_owned()
3958}
3959
3960fn query_rows<T, F>(connection: &Connection, sql: &str, params: Vec<SqlValue>, mut map: F) -> Result<Vec<T>>
3961where
3962 F: FnMut(&SqlRow<'_>) -> Result<T>,
3963{
3964 let mut statement = connection
3965 .prepare(sql)
3966 .with_context(|| format!("failed to prepare SQL query: {sql}"))?;
3967 let mut rows = statement
3968 .query(params_from_iter(params))
3969 .with_context(|| format!("failed to execute SQL query: {sql}"))?;
3970 let mut result = Vec::new();
3971
3972 while let Some(row) = rows.next().context("failed to read SQL row")? {
3973 result.push(map(row)?);
3974 }
3975
3976 Ok(result)
3977}
3978
3979fn query_optional_value<T, F>(connection: &Connection, sql: &str, params: Vec<SqlValue>, map: F) -> Result<Option<T>>
3980where
3981 F: FnOnce(&SqlRow<'_>) -> Result<T>,
3982{
3983 let mut statement = connection
3984 .prepare(sql)
3985 .with_context(|| format!("failed to prepare SQL query: {sql}"))?;
3986 let mut rows = statement
3987 .query(params_from_iter(params))
3988 .with_context(|| format!("failed to execute SQL query: {sql}"))?;
3989
3990 match rows.next().context("failed to read SQL row")? {
3991 Some(row) => Ok(Some(map(row)?)),
3992 None => Ok(None),
3993 }
3994}
3995
3996fn row_ref<'a>(row: &'a SqlRow<'_>, index: usize) -> Result<ValueRef<'a>> {
3997 row.get_ref(index)
3998 .with_context(|| format!("failed to read SQL column {index}"))
3999}
4000
4001fn row_string(row: &SqlRow<'_>, index: usize) -> Result<String> {
4002 match row_ref(row, index)? {
4003 ValueRef::Text(value) => std::str::from_utf8(value)
4004 .context("SQL text column was not valid UTF-8")
4005 .map(str::to_owned),
4006 ValueRef::Integer(value) => Ok(value.to_string()),
4007 ValueRef::Real(value) => Ok(value.to_string()),
4008 ValueRef::Blob(value) => String::from_utf8(value.to_vec()).context("SQL blob column was not valid UTF-8"),
4009 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4010 }
4011}
4012
4013fn row_opt_string(row: &SqlRow<'_>, index: usize) -> Result<Option<String>> {
4014 match row_ref(row, index)? {
4015 ValueRef::Null => Ok(None),
4016 ValueRef::Text(value) => Ok(Some(
4017 std::str::from_utf8(value)
4018 .context("SQL text column was not valid UTF-8")?
4019 .to_owned(),
4020 )),
4021 ValueRef::Integer(value) => Ok(Some(value.to_string())),
4022 ValueRef::Real(value) => Ok(Some(value.to_string())),
4023 ValueRef::Blob(value) => Ok(Some(
4024 String::from_utf8(value.to_vec()).context("SQL blob column was not valid UTF-8")?,
4025 )),
4026 }
4027}
4028
4029fn row_i64(row: &SqlRow<'_>, index: usize) -> Result<i64> {
4030 match row_ref(row, index)? {
4031 ValueRef::Integer(value) => Ok(value),
4032 ValueRef::Text(value) => std::str::from_utf8(value)
4033 .context("SQL text column was not valid UTF-8")?
4034 .parse::<i64>()
4035 .with_context(|| format!("failed to parse integer from SQL text column {index}")),
4036 ValueRef::Real(value) => f64_to_i64_checked(value, index),
4037 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4038 ValueRef::Blob(_) => bail!("SQL column {index} was unexpectedly a blob"),
4039 }
4040}
4041
4042fn row_hex_string(row: &SqlRow<'_>, index: usize) -> Result<String> {
4043 match row_ref(row, index)? {
4044 ValueRef::Blob(value) => Ok(bytes_to_hex(value)),
4045 ValueRef::Text(value) => Ok(std::str::from_utf8(value)
4046 .context("SQL text column was not valid UTF-8")?
4047 .to_ascii_lowercase()),
4048 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4049 ValueRef::Integer(value) => Ok(format!("{value:x}")),
4050 ValueRef::Real(value) => Ok(format!("{:x}", f64_to_i64_checked(value, index)?)),
4051 }
4052}
4053
4054fn row_opt_hex_string(row: &SqlRow<'_>, index: usize) -> Result<Option<String>> {
4055 match row_ref(row, index)? {
4056 ValueRef::Null => Ok(None),
4057 ValueRef::Blob(value) => Ok(Some(bytes_to_hex(value))),
4058 ValueRef::Text(value) => Ok(Some(
4059 std::str::from_utf8(value)
4060 .context("SQL text column was not valid UTF-8")?
4061 .to_ascii_lowercase(),
4062 )),
4063 ValueRef::Integer(value) => Ok(Some(format!("{value:x}"))),
4064 ValueRef::Real(value) => Ok(Some(format!("{:x}", f64_to_i64_checked(value, index)?))),
4065 }
4066}
4067
4068fn f64_to_i64_checked(value: f64, index: usize) -> Result<i64> {
4069 if !value.is_finite() {
4070 bail!("SQL column {index} contained a non-finite floating-point value");
4071 }
4072
4073 if value.fract() != 0.0 {
4074 bail!("SQL column {index} contained a non-integer floating-point value");
4075 }
4076
4077 if !(-9_223_372_036_854_775_808.0..=9_223_372_036_854_775_807.0).contains(&value) {
4078 bail!("SQL column {index} contained a floating-point value outside the i64 range");
4079 }
4080
4081 format!("{value:.0}")
4082 .parse::<i64>()
4083 .with_context(|| format!("failed to parse integer from SQL floating-point column {index}"))
4084}
4085
4086fn bytes_to_hex(bytes: &[u8]) -> String {
4087 let mut result = String::with_capacity(bytes.len() * 2);
4088 for byte in bytes {
4089 result.push_str(&format!("{byte:02x}"));
4090 }
4091 result
4092}
4093
4094fn query_v2_matches(
4095 connection: &Connection,
4096 query: &PackageQuery,
4097 semantics: SearchSemantics,
4098) -> Result<(Vec<V2SearchRow>, bool)> {
4099 let (where_clause, params) = build_preindexed_where_clause(query, true, semantics);
4100 let limit = source_fetch_results(query, semantics);
4101 let sql = format!(
4102 "SELECT rowid, id, name, moniker, latest_version, hash \
4103 FROM packages WHERE {where_clause} LIMIT {}",
4104 limit + 1
4105 );
4106 let mut rows = query_rows(
4107 connection,
4108 &sql,
4109 params.into_iter().map(SqlValue::Text).collect(),
4110 |row| {
4111 Ok(V2SearchRow {
4112 package_rowid: row_i64(row, 0)?,
4113 id: row_string(row, 1)?,
4114 name: row_string(row, 2)?,
4115 moniker: row_opt_string(row, 3)?,
4116 version: row_string(row, 4)?,
4117 package_hash: row_hex_string(row, 5)?,
4118 match_criteria: None,
4119 })
4120 },
4121 )?;
4122 for row in &mut rows {
4123 row.match_criteria = infer_preindexed_match_criteria_v2(connection, row, query, semantics)?;
4124 }
4125 let truncated = rows.len() > limit;
4126 rows.truncate(limit);
4127 Ok((rows, truncated))
4128}
4129
4130fn query_v1_matches(
4131 connection: &Connection,
4132 query: &PackageQuery,
4133 semantics: SearchSemantics,
4134) -> Result<(Vec<V1SearchRow>, bool)> {
4135 let (where_clause, params) = build_preindexed_where_clause(query, false, semantics);
4136 let limit = source_fetch_results(query, semantics);
4137 let sql = format!(
4138 "SELECT manifest.rowid, manifest.id, versions.version, channels.channel, ids.id, names.name, monikers.moniker \
4139 FROM manifest \
4140 JOIN ids ON manifest.id = ids.rowid \
4141 JOIN names ON manifest.name = names.rowid \
4142 LEFT JOIN monikers ON manifest.moniker = monikers.rowid \
4143 JOIN versions ON manifest.version = versions.rowid \
4144 JOIN channels ON manifest.channel = channels.rowid \
4145 WHERE {where_clause} LIMIT {}",
4146 limit + 1
4147 );
4148 let mut rows = query_rows(
4149 connection,
4150 &sql,
4151 params.into_iter().map(SqlValue::Text).collect(),
4152 |row| {
4153 Ok(V1SearchRow {
4154 manifest_rowid: row_i64(row, 0)?,
4155 package_rowid: row_i64(row, 1)?,
4156 version: row_string(row, 2)?,
4157 channel: row_string(row, 3)?,
4158 id: row_string(row, 4)?,
4159 name: row_string(row, 5)?,
4160 moniker: row_opt_string(row, 6)?,
4161 match_criteria: None,
4162 })
4163 },
4164 )?;
4165 for row in &mut rows {
4166 row.match_criteria = infer_preindexed_match_criteria_v1(connection, row, query, semantics)?;
4167 }
4168 let truncated = rows.len() > limit;
4169 rows.truncate(limit);
4170 Ok((rows, truncated))
4171}
4172
4173fn group_v1_rows(rows: Vec<V1SearchRow>) -> Vec<V1SearchRow> {
4174 let mut grouped = BTreeMap::<i64, V1SearchRow>::new();
4175
4176 for row in rows {
4177 match grouped.get(&row.package_rowid) {
4178 Some(existing)
4179 if compare_version_and_channel(&existing.version, &existing.channel, &row.version, &row.channel)
4180 != Ordering::Less => {}
4181 _ => {
4182 grouped.insert(row.package_rowid, row);
4183 }
4184 }
4185 }
4186
4187 grouped.into_values().collect()
4188}
4189
4190fn query_v1_versions(connection: &Connection, package_rowid: i64) -> Result<Vec<V1VersionRow>> {
4191 let has_hash = table_has_column(connection, "manifest", "hash")?;
4192 let hash_sql = if has_hash { "manifest.hash" } else { "NULL" };
4193 let sql = format!(
4194 "SELECT versions.version, channels.channel, manifest.pathpart, {hash_sql} \
4195 FROM manifest \
4196 JOIN versions ON manifest.version = versions.rowid \
4197 JOIN channels ON manifest.channel = channels.rowid \
4198 WHERE manifest.id = ?1"
4199 );
4200 query_rows(connection, &sql, vec![SqlValue::Integer(package_rowid)], |row| {
4201 Ok(V1VersionRow {
4202 version: row_string(row, 0)?,
4203 channel: row_string(row, 1)?,
4204 pathpart_id: row_i64(row, 2)?,
4205 manifest_hash: row_opt_hex_string(row, 3)?,
4206 })
4207 })
4208}
4209
4210fn resolve_v1_relative_path(connection: &Connection, pathpart_id: i64) -> Result<String> {
4211 let mut parts = Vec::new();
4212 let mut current = Some(pathpart_id);
4213
4214 while let Some(id) = current {
4215 let (parent, pathpart) = query_optional_value(
4216 connection,
4217 "SELECT parent, pathpart FROM pathparts WHERE rowid = ?1",
4218 vec![SqlValue::Integer(id)],
4219 |row| {
4220 Ok((
4221 row_opt_string(row, 0)?
4222 .and_then(|value| value.parse::<i64>().ok())
4223 .filter(|value| *value != 0),
4224 row_string(row, 1)?,
4225 ))
4226 },
4227 )?
4228 .ok_or_else(|| anyhow!("failed to resolve pathpart {id}"))?;
4229 parts.push(pathpart);
4230 current = parent;
4231 }
4232
4233 parts.reverse();
4234 Ok(parts.join("/"))
4235}
4236
4237fn package_version_data_relative_path(package_id: &str, package_hash: &str) -> String {
4238 format!(
4239 "packages/{}/{}/versionData.mszyml",
4240 package_id,
4241 package_hash[..package_hash.len().min(8)].to_ascii_lowercase()
4242 )
4243}
4244
4245fn can_fallback_to_v1(error: &anyhow::Error) -> bool {
4246 let message = format!("{error:#}").to_ascii_lowercase();
4247 message.contains("no such table") && message.contains("packages")
4248}
4249
4250fn table_has_column(connection: &Connection, table: &str, column: &str) -> Result<bool> {
4251 for value in query_rows(connection, &format!("PRAGMA table_info({table})"), Vec::new(), |row| {
4252 row_string(row, 1)
4253 })? {
4254 if value.eq_ignore_ascii_case(column) {
4255 return Ok(true);
4256 }
4257 }
4258
4259 Ok(false)
4260}
4261
4262fn build_preindexed_where_clause(query: &PackageQuery, v2: bool, semantics: SearchSemantics) -> (String, Vec<String>) {
4263 let rowid_column = if v2 { "packages.rowid" } else { "manifest.rowid" };
4264 let id_column = if v2 { "id" } else { "ids.id" };
4265 let name_column = if v2 { "name" } else { "names.name" };
4266 let moniker_column = if v2 { "moniker" } else { "monikers.moniker" };
4267 let exact_match = query.exact || semantics == SearchSemantics::Single;
4268 let mut params = Vec::new();
4269
4270 if let Some(value) = &query.id {
4271 return (
4272 single_field_condition(id_column, value, exact_match, &mut params),
4273 params,
4274 );
4275 }
4276 if let Some(value) = &query.name {
4277 return (
4278 single_field_condition(name_column, value, exact_match, &mut params),
4279 params,
4280 );
4281 }
4282 if let Some(value) = &query.moniker {
4283 return (
4284 single_field_condition(moniker_column, value, exact_match, &mut params),
4285 params,
4286 );
4287 }
4288 if let Some(value) = &query.tag {
4289 return (
4290 mapped_field_condition(v2, "tag", value, rowid_column, true, &mut params),
4291 params,
4292 );
4293 }
4294 if let Some(value) = &query.command {
4295 return (
4296 mapped_field_condition(v2, "command", value, rowid_column, true, &mut params),
4297 params,
4298 );
4299 }
4300 if let Some(value) = &query.query {
4301 if exact_match {
4302 let conditions = [
4303 single_field_condition(id_column, value, true, &mut params),
4304 single_field_condition(name_column, value, true, &mut params),
4305 single_field_condition(moniker_column, value, true, &mut params),
4306 ];
4307 return (format!("({})", conditions.join(" OR ")), params);
4308 }
4309
4310 let mut conditions = vec![
4311 single_field_condition(id_column, value, false, &mut params),
4312 single_field_condition(name_column, value, false, &mut params),
4313 single_field_condition(moniker_column, value, false, &mut params),
4314 ];
4315 if semantics == SearchSemantics::Many {
4316 conditions.push(mapped_field_condition(
4317 v2,
4318 "tag",
4319 value,
4320 rowid_column,
4321 false,
4322 &mut params,
4323 ));
4324 conditions.push(mapped_field_condition(
4325 v2,
4326 "command",
4327 value,
4328 rowid_column,
4329 false,
4330 &mut params,
4331 ));
4332 }
4333 return (format!("({})", conditions.join(" OR ")), params);
4334 }
4335
4336 ("1 = 1".to_owned(), Vec::new())
4337}
4338
4339fn single_field_condition(column: &str, value: &str, exact: bool, params: &mut Vec<String>) -> String {
4340 params.push(match_parameter(value, exact));
4341 format!("{column} LIKE ?{}", params.len())
4342}
4343
4344fn mapped_field_condition(
4345 v2: bool,
4346 value_name: &str,
4347 value: &str,
4348 rowid_column: &str,
4349 exact: bool,
4350 params: &mut Vec<String>,
4351) -> String {
4352 let (table_name, map_table_name, map_value_column, map_owner_column) = if v2 {
4353 (
4354 format!("{value_name}s2"),
4355 format!("{value_name}s2_map"),
4356 value_name.to_owned(),
4357 "package".to_owned(),
4358 )
4359 } else {
4360 (
4361 format!("{value_name}s"),
4362 format!("{value_name}s_map"),
4363 value_name.to_owned(),
4364 "manifest".to_owned(),
4365 )
4366 };
4367 params.push(match_parameter(value, exact));
4368 let parameter = params.len();
4369 format!(
4370 "EXISTS (SELECT 1 FROM {map_table_name} JOIN {table_name} ON \
4371 {map_table_name}.{value_name} = {table_name}.rowid \
4372 WHERE {map_table_name}.{map_owner_column} = {rowid_column} \
4373 AND {table_name}.{map_value_column} LIKE ?{parameter})"
4374 )
4375}
4376
4377fn match_parameter(value: &str, exact: bool) -> String {
4378 if exact { value.to_owned() } else { format!("%{value}%") }
4379}
4380
4381fn build_rest_search_body(
4382 query: &PackageQuery,
4383 info: &RestInformation,
4384 semantics: SearchSemantics,
4385) -> Result<JsonValue> {
4386 let mut root = serde_json::Map::new();
4387 root.insert(
4388 "MaximumResults".to_owned(),
4389 JsonValue::from(source_fetch_results(query, semantics) as u64),
4390 );
4391 let exact_match = query.exact || semantics == SearchSemantics::Single;
4392
4393 if let Some(value) = &query.query {
4394 if semantics == SearchSemantics::Single {
4395 let mut filters = vec![
4396 rest_filter("PackageIdentifier", value, true),
4397 rest_filter("PackageName", value, true),
4398 rest_filter("Moniker", value, true),
4399 ];
4400 append_required_rest_filters(&mut filters, info);
4401 root.insert("Filters".to_owned(), JsonValue::Array(filters));
4402 return Ok(JsonValue::Object(root));
4403 }
4404
4405 root.insert(
4406 "Query".to_owned(),
4407 serde_json::json!({
4408 "KeyWord": value,
4409 "MatchType": if exact_match { "Exact" } else { "Substring" },
4410 }),
4411 );
4412 }
4413
4414 let mut filters = Vec::new();
4415 if let Some(value) = &query.id {
4416 filters.push(rest_filter("PackageIdentifier", value, exact_match));
4417 }
4418 if let Some(value) = &query.name {
4419 filters.push(rest_filter("PackageName", value, exact_match));
4420 }
4421 if let Some(value) = &query.moniker {
4422 filters.push(rest_filter("Moniker", value, exact_match));
4423 }
4424 if let Some(value) = &query.tag {
4425 filters.push(rest_filter("Tag", value, true));
4426 }
4427 if let Some(value) = &query.command {
4428 filters.push(rest_filter("Command", value, true));
4429 }
4430 append_required_rest_filters(&mut filters, info);
4431
4432 if !filters.is_empty() {
4433 root.insert("Filters".to_owned(), JsonValue::Array(filters));
4434 }
4435
4436 Ok(JsonValue::Object(root))
4437}
4438
4439fn append_required_rest_filters(filters: &mut Vec<JsonValue>, info: &RestInformation) {
4440 if info
4441 .required_package_match_fields
4442 .iter()
4443 .any(|field| field.eq_ignore_ascii_case("Market"))
4444 {
4445 filters.push(rest_filter("Market", &default_market(), true));
4446 }
4447}
4448
4449fn rest_filter(field: &str, value: &str, exact: bool) -> JsonValue {
4450 serde_json::json!({
4451 "PackageMatchField": field,
4452 "RequestMatch": {
4453 "KeyWord": value,
4454 "MatchType": if exact { "Exact" } else { "Substring" },
4455 }
4456 })
4457}
4458
4459fn max_results(query: &PackageQuery) -> usize {
4460 query.count.unwrap_or(DEFAULT_MAX_RESULTS).max(1)
4461}
4462
4463fn source_fetch_results(query: &PackageQuery, semantics: SearchSemantics) -> usize {
4464 match semantics {
4465 SearchSemantics::Many => max_results(query).max(DEFAULT_MAX_RESULTS),
4466 SearchSemantics::Single => max_results(query),
4467 }
4468}
4469
4470fn infer_preindexed_match_criteria_v2(
4471 connection: &Connection,
4472 row: &V2SearchRow,
4473 query: &PackageQuery,
4474 semantics: SearchSemantics,
4475) -> Result<Option<String>> {
4476 infer_match_criteria(
4477 &row.id,
4478 &row.name,
4479 row.moniker.as_deref(),
4480 query,
4481 semantics,
4482 |value| {
4483 find_mapped_value_v2(
4484 connection,
4485 "tags2",
4486 "tags2_map",
4487 "tag",
4488 row.package_rowid,
4489 value,
4490 query.exact,
4491 )
4492 },
4493 |value| {
4494 find_mapped_value_v2(
4495 connection,
4496 "commands2",
4497 "commands2_map",
4498 "command",
4499 row.package_rowid,
4500 value,
4501 query.exact,
4502 )
4503 },
4504 )
4505}
4506
4507fn infer_preindexed_match_criteria_v1(
4508 connection: &Connection,
4509 row: &V1SearchRow,
4510 query: &PackageQuery,
4511 semantics: SearchSemantics,
4512) -> Result<Option<String>> {
4513 infer_match_criteria(
4514 &row.id,
4515 &row.name,
4516 row.moniker.as_deref(),
4517 query,
4518 semantics,
4519 |value| {
4520 find_mapped_value_v1(
4521 connection,
4522 "tags",
4523 "tags_map",
4524 "tag",
4525 row.manifest_rowid,
4526 value,
4527 query.exact,
4528 )
4529 },
4530 |value| {
4531 find_mapped_value_v1(
4532 connection,
4533 "commands",
4534 "commands_map",
4535 "command",
4536 row.manifest_rowid,
4537 value,
4538 query.exact,
4539 )
4540 },
4541 )
4542}
4543
4544fn infer_match_criteria<FTag, FCommand>(
4545 id: &str,
4546 name: &str,
4547 moniker: Option<&str>,
4548 query: &PackageQuery,
4549 semantics: SearchSemantics,
4550 find_tag: FTag,
4551 find_command: FCommand,
4552) -> Result<Option<String>>
4553where
4554 FTag: FnOnce(&str) -> Result<Option<String>>,
4555 FCommand: FnOnce(&str) -> Result<Option<String>>,
4556{
4557 if let Some(value) = &query.tag {
4558 return Ok(Some(format_match_criteria("Tag", value)));
4559 }
4560 if let Some(value) = &query.command {
4561 return Ok(Some(format_match_criteria("Command", value)));
4562 }
4563 if let Some(value) = &query.moniker {
4564 return Ok(Some(format_match_criteria("Moniker", moniker.unwrap_or(value))));
4565 }
4566 if semantics == SearchSemantics::Single {
4567 return Ok(None);
4568 }
4569 if let Some(value) = &query.query {
4570 if matches_text(id, value, query.exact) || matches_text(name, value, query.exact) {
4571 return Ok(None);
4572 }
4573 if let Some(moniker_value) = moniker.filter(|candidate| matches_text(candidate, value, query.exact)) {
4574 return Ok(Some(format_match_criteria("Moniker", moniker_value)));
4575 }
4576 if let Some(tag) = find_tag(value)? {
4577 return Ok(Some(format_match_criteria("Tag", &tag)));
4578 }
4579 if let Some(command) = find_command(value)? {
4580 return Ok(Some(format_match_criteria("Command", &command)));
4581 }
4582 }
4583 Ok(None)
4584}
4585
4586fn find_mapped_value_v2(
4587 connection: &Connection,
4588 table_name: &str,
4589 map_table_name: &str,
4590 value_name: &str,
4591 package_rowid: i64,
4592 query: &str,
4593 exact: bool,
4594) -> Result<Option<String>> {
4595 let sql = format!(
4596 "SELECT {table_name}.{value_name} FROM {map_table_name} \
4597 JOIN {table_name} ON {map_table_name}.{value_name} = {table_name}.rowid \
4598 WHERE {map_table_name}.package = ?1 AND {table_name}.{value_name} LIKE ?2"
4599 );
4600 let values = query_rows(
4601 connection,
4602 &sql,
4603 vec![
4604 SqlValue::Integer(package_rowid),
4605 SqlValue::Text(match_parameter(query, exact)),
4606 ],
4607 |row| row_string(row, 0),
4608 )?;
4609 Ok(select_best_text_match(values, query, exact))
4610}
4611
4612fn find_mapped_value_v1(
4613 connection: &Connection,
4614 table_name: &str,
4615 map_table_name: &str,
4616 value_name: &str,
4617 manifest_rowid: i64,
4618 query: &str,
4619 exact: bool,
4620) -> Result<Option<String>> {
4621 let sql = format!(
4622 "SELECT {table_name}.{value_name} FROM {map_table_name} \
4623 JOIN {table_name} ON {map_table_name}.{value_name} = {table_name}.rowid \
4624 WHERE {map_table_name}.manifest = ?1 AND {table_name}.{value_name} LIKE ?2"
4625 );
4626 let values = query_rows(
4627 connection,
4628 &sql,
4629 vec![
4630 SqlValue::Integer(manifest_rowid),
4631 SqlValue::Text(match_parameter(query, exact)),
4632 ],
4633 |row| row_string(row, 0),
4634 )?;
4635 Ok(select_best_text_match(values, query, exact))
4636}
4637
4638fn rest_match_criteria(item: &JsonValue, query: &PackageQuery, semantics: SearchSemantics) -> Option<String> {
4639 if let Some(value) = &query.tag {
4640 return Some(format_match_criteria("Tag", value));
4641 }
4642 if let Some(value) = &query.command {
4643 return Some(format_match_criteria("Command", value));
4644 }
4645 if let Some(value) = &query.moniker {
4646 return Some(format_match_criteria("Moniker", value));
4647 }
4648 if semantics == SearchSemantics::Single {
4649 return None;
4650 }
4651 let value = query.query.as_deref()?;
4652 if json_string(item, "PackageIdentifier")
4653 .as_deref()
4654 .is_some_and(|candidate| matches_text(candidate, value, query.exact))
4655 || json_string(item, "PackageName")
4656 .as_deref()
4657 .is_some_and(|candidate| matches_text(candidate, value, query.exact))
4658 {
4659 return None;
4660 }
4661 json_string(item, "Moniker")
4662 .filter(|candidate| matches_text(candidate, value, query.exact))
4663 .map(|candidate| format_match_criteria("Moniker", &candidate))
4664}
4665
4666fn format_match_criteria(field: &str, value: &str) -> String {
4667 format!("{field}: {value}")
4668}
4669
4670fn matches_text(candidate: &str, query: &str, exact: bool) -> bool {
4671 if exact {
4672 candidate.eq_ignore_ascii_case(query)
4673 } else {
4674 candidate.to_ascii_lowercase().contains(&query.to_ascii_lowercase())
4675 }
4676}
4677
4678fn search_match_sort_score(candidate: &SearchMatch, query: &PackageQuery) -> usize {
4679 let mut score = 0;
4680
4681 if let Some(value) = &query.query {
4682 score = score.max(score_text_match(&candidate.name, value, query.exact, 140, 50, 30));
4683 score = score.max(score_text_match(&candidate.id, value, query.exact, 135, 45, 25));
4684 if let Some(moniker) = candidate.moniker.as_deref() {
4685 score = score.max(score_text_match(moniker, value, query.exact, 130, 55, 35));
4686 }
4687 }
4688
4689 if let Some(value) = &query.id {
4690 score = score.max(score_text_match(&candidate.id, value, query.exact, 220, 200, 180));
4691 }
4692 if let Some(value) = &query.name {
4693 score = score.max(score_text_match(&candidate.name, value, query.exact, 210, 190, 170));
4694 }
4695 if let Some(value) = &query.moniker
4696 && let Some(moniker) = candidate.moniker.as_deref()
4697 {
4698 score = score.max(score_text_match(moniker, value, query.exact, 205, 185, 165));
4699 }
4700 if let Some(value) = &query.tag
4701 && let Some(("Tag", matched_value)) = candidate.match_criteria.as_deref().and_then(parse_match_criteria)
4702 {
4703 score = score.max(score_text_match(matched_value, value, query.exact, 160, 150, 140));
4704 }
4705 if let Some(value) = &query.command
4706 && let Some(("Command", matched_value)) = candidate.match_criteria.as_deref().and_then(parse_match_criteria)
4707 {
4708 score = score.max(score_text_match(matched_value, value, query.exact, 155, 145, 135));
4709 }
4710
4711 if matches!(candidate.source_kind, SourceKind::PreIndexed) {
4712 score += 5;
4713 }
4714 if search_match_has_unknown_version(candidate) {
4715 score = score.saturating_sub(10);
4716 }
4717
4718 score
4719}
4720
4721fn search_match_has_unknown_version(candidate: &SearchMatch) -> bool {
4722 candidate
4723 .version
4724 .as_deref()
4725 .is_some_and(|value| value.eq_ignore_ascii_case("Unknown"))
4726}
4727
4728fn resolve_manifest_path(manifest_path: &Path) -> Result<PathBuf> {
4729 if manifest_path.is_file() {
4730 return Ok(manifest_path.to_path_buf());
4731 }
4732
4733 if !manifest_path.is_dir() {
4734 bail!("Manifest path not found: {}", manifest_path.display());
4735 }
4736
4737 let candidate = fs::read_dir(manifest_path)?
4738 .filter_map(|entry| entry.ok())
4739 .map(|entry| entry.path())
4740 .filter(|path| {
4741 path.extension()
4742 .and_then(|value| value.to_str())
4743 .is_some_and(|ext| ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml"))
4744 })
4745 .min();
4746
4747 candidate.ok_or_else(|| anyhow!("No manifest file found under: {}", manifest_path.display()))
4748}
4749
4750fn score_text_match(
4751 candidate: &str,
4752 query: &str,
4753 exact: bool,
4754 exact_score: usize,
4755 prefix_score: usize,
4756 substring_score: usize,
4757) -> usize {
4758 if candidate.eq_ignore_ascii_case(query) {
4759 return exact_score;
4760 }
4761 if exact {
4762 return 0;
4763 }
4764
4765 let candidate_lower = candidate.to_ascii_lowercase();
4766 let query_lower = query.to_ascii_lowercase();
4767 if candidate_lower.starts_with(&query_lower) {
4768 prefix_score
4769 } else if candidate_lower.contains(&query_lower) {
4770 substring_score
4771 } else {
4772 0
4773 }
4774}
4775
4776fn select_best_text_match<I>(values: I, query: &str, exact: bool) -> Option<String>
4777where
4778 I: IntoIterator<Item = String>,
4779{
4780 values
4781 .into_iter()
4782 .filter_map(|candidate| {
4783 let score = score_text_match(&candidate, query, exact, 3, 2, 1);
4784 (score > 0).then_some((score, candidate))
4785 })
4786 .max_by(|(left_score, left_value), (right_score, right_value)| {
4787 left_score
4788 .cmp(right_score)
4789 .then_with(|| right_value.len().cmp(&left_value.len()))
4790 .then_with(|| right_value.to_ascii_lowercase().cmp(&left_value.to_ascii_lowercase()))
4791 })
4792 .map(|(_, candidate)| candidate)
4793}
4794
4795fn parse_match_criteria(criteria: &str) -> Option<(&str, &str)> {
4796 let (field, value) = criteria.split_once(": ")?;
4797 Some((field, value))
4798}
4799
4800fn parse_rest_versions(item: &JsonValue) -> Result<Vec<VersionKey>> {
4801 let versions = item
4802 .get("Versions")
4803 .and_then(JsonValue::as_array)
4804 .ok_or_else(|| anyhow!("REST search result missing Versions"))?;
4805 let mut result = Vec::new();
4806 for version in versions {
4807 let value = json_string(version, "PackageVersion")
4808 .ok_or_else(|| anyhow!("REST version entry missing PackageVersion"))?;
4809 let channel = json_string(version, "Channel").unwrap_or_default();
4810 result.push(VersionKey {
4811 version: value,
4812 channel,
4813 });
4814 }
4815 Ok(result)
4816}
4817
4818fn parse_yaml_manifest_bundle(bytes: &[u8]) -> Result<(Manifest, JsonValue)> {
4819 let mut merged = YamlMapping::new();
4820 let mut documents = Vec::new();
4821 for document in serde_yaml::Deserializer::from_slice(bytes) {
4822 let value = YamlValue::deserialize(document).context("failed to deserialize YAML document")?;
4823 documents.push(serde_json::to_value(&value).context("failed to convert YAML manifest document")?);
4824 if let Some(mapping) = value.as_mapping() {
4825 merge_yaml_mapping(&mut merged, mapping);
4826 }
4827 }
4828
4829 let id = yaml_string_from_root(&merged, "PackageIdentifier")
4830 .ok_or_else(|| anyhow!("manifest missing PackageIdentifier"))?;
4831 let version =
4832 yaml_string_from_root(&merged, "PackageVersion").ok_or_else(|| anyhow!("manifest missing PackageVersion"))?;
4833 let name = yaml_localized_string(&merged, "PackageName").ok_or_else(|| anyhow!("manifest missing PackageName"))?;
4834
4835 let installers = parse_yaml_installers(&merged);
4836
4837 Ok((
4838 Manifest {
4839 id,
4840 name,
4841 version,
4842 channel: yaml_string_from_root(&merged, "Channel").unwrap_or_default(),
4843 publisher: yaml_localized_string(&merged, "Publisher"),
4844 description: yaml_localized_string(&merged, "Description")
4845 .or_else(|| yaml_localized_string(&merged, "ShortDescription")),
4846 moniker: yaml_string_from_root(&merged, "Moniker"),
4847 package_url: yaml_localized_string(&merged, "PackageUrl"),
4848 publisher_url: yaml_localized_string(&merged, "PublisherUrl"),
4849 publisher_support_url: yaml_localized_string(&merged, "PublisherSupportUrl"),
4850 license: yaml_localized_string(&merged, "License"),
4851 license_url: yaml_localized_string(&merged, "LicenseUrl"),
4852 privacy_url: yaml_localized_string(&merged, "PrivacyUrl"),
4853 author: yaml_localized_string(&merged, "Author"),
4854 copyright: yaml_localized_string(&merged, "Copyright"),
4855 copyright_url: yaml_localized_string(&merged, "CopyrightUrl"),
4856 release_notes: yaml_localized_string(&merged, "ReleaseNotes"),
4857 release_notes_url: yaml_localized_string(&merged, "ReleaseNotesUrl"),
4858 tags: yaml_string_list(&merged, "Tags"),
4859 agreements: yaml_agreement_list(&merged),
4860 package_dependencies: yaml_package_dependencies(&merged),
4861 documentation: yaml_documentation_list(&merged),
4862 installers,
4863 },
4864 collapse_structured_document(&JsonValue::Array(documents)),
4865 ))
4866}
4867
4868fn parse_yaml_manifest(bytes: &[u8]) -> Result<Manifest> {
4869 parse_yaml_manifest_bundle(bytes).map(|(manifest, _)| manifest)
4870}
4871
4872fn parse_rest_manifest(bytes: &[u8], package_id: &str, version: &str, channel: &str) -> Result<(Manifest, JsonValue)> {
4873 let root = serde_json::from_slice::<JsonValue>(bytes).context("failed to deserialize REST manifest JSON")?;
4874 let data = root
4875 .get("Data")
4876 .ok_or_else(|| anyhow!("REST manifest response missing Data"))?;
4877 let versions = data
4878 .get("Versions")
4879 .and_then(JsonValue::as_array)
4880 .ok_or_else(|| anyhow!("REST manifest response missing Versions"))?;
4881 let selected = versions
4882 .iter()
4883 .find(|item| {
4884 json_string(item, "PackageVersion").as_deref() == Some(version)
4885 && json_string(item, "Channel").unwrap_or_default() == channel
4886 })
4887 .or_else(|| versions.first())
4888 .ok_or_else(|| anyhow!("REST manifest response did not contain a version payload"))?;
4889
4890 let default_locale = selected
4891 .get("DefaultLocale")
4892 .ok_or_else(|| anyhow!("REST manifest response missing DefaultLocale"))?;
4893 let name = json_string(default_locale, "PackageName")
4894 .ok_or_else(|| anyhow!("REST manifest response missing PackageName"))?;
4895 let installer_switch_defaults = json_installer_switches(selected);
4896 let top_platforms = json_string_list(selected, "Platform");
4897 let top_minimum_os_version = json_string(selected, "MinimumOSVersion");
4898
4899 let installers = selected
4900 .get("Installers")
4901 .and_then(JsonValue::as_array)
4902 .map(|items| {
4903 items
4904 .iter()
4905 .map(|item| Installer {
4906 platforms: {
4907 let platforms = json_string_list(item, "Platform");
4908 if platforms.is_empty() {
4909 top_platforms.clone()
4910 } else {
4911 platforms
4912 }
4913 },
4914 minimum_os_version: json_string(item, "MinimumOSVersion")
4915 .or_else(|| top_minimum_os_version.clone()),
4916 architecture: json_string(item, "Architecture"),
4917 installer_type: json_string(item, "InstallerType"),
4918 url: json_string(item, "InstallerUrl"),
4919 sha256: json_string(item, "InstallerSha256"),
4920 product_code: json_string(item, "ProductCode"),
4921 locale: json_string(item, "InstallerLocale"),
4922 scope: json_string(item, "Scope"),
4923 release_date: json_string(item, "ReleaseDate"),
4924 package_family_name: json_string(item, "PackageFamilyName"),
4925 upgrade_code: json_string(item, "UpgradeCode"),
4926 switches: json_installer_switches(item).with_fallback(&installer_switch_defaults),
4927 commands: json_string_list(item, "Commands"),
4928 package_dependencies: json_package_dependencies(item),
4929 })
4930 .collect::<Vec<_>>()
4931 })
4932 .unwrap_or_default();
4933
4934 let manifest = Manifest {
4935 id: package_id.to_owned(),
4936 name,
4937 version: version.to_owned(),
4938 channel: channel.to_owned(),
4939 publisher: json_string(default_locale, "Publisher"),
4940 description: json_string(default_locale, "Description")
4941 .or_else(|| json_string(default_locale, "ShortDescription")),
4942 moniker: json_string(default_locale, "Moniker"),
4943 package_url: json_string(default_locale, "PackageUrl"),
4944 publisher_url: json_string(default_locale, "PublisherUrl"),
4945 publisher_support_url: json_string(default_locale, "PublisherSupportUrl"),
4946 license: json_string(default_locale, "License"),
4947 license_url: json_string(default_locale, "LicenseUrl"),
4948 privacy_url: json_string(default_locale, "PrivacyUrl"),
4949 author: json_string(default_locale, "Author"),
4950 copyright: json_string(default_locale, "Copyright"),
4951 copyright_url: json_string(default_locale, "CopyrightUrl"),
4952 release_notes: json_string(default_locale, "ReleaseNotes"),
4953 release_notes_url: json_string(default_locale, "ReleaseNotesUrl"),
4954 tags: json_string_list(default_locale, "Tags"),
4955 agreements: json_agreement_list(default_locale),
4956 package_dependencies: json_package_dependencies(selected),
4957 documentation: json_documentation_list(default_locale),
4958 installers,
4959 };
4960
4961 Ok((
4962 manifest,
4963 build_rest_manifest_documents(data, selected, default_locale, package_id, version, channel),
4964 ))
4965}
4966
4967fn collapse_structured_document(document: &JsonValue) -> JsonValue {
4968 match document {
4969 JsonValue::Array(documents) if documents.len() == 1 && is_merged_manifest_document(&documents[0]) => {
4970 merge_manifest_documents(&split_merged_manifest_document(&documents[0]))
4971 }
4972 JsonValue::Array(documents) => merge_manifest_documents(documents),
4973 JsonValue::Object(_) if is_merged_manifest_document(document) => {
4974 merge_manifest_documents(&split_merged_manifest_document(document))
4975 }
4976 _ => document.clone(),
4977 }
4978}
4979
4980fn collapse_structured_documents(documents: &[JsonValue]) -> Vec<JsonValue> {
4981 documents.iter().map(collapse_structured_document).collect()
4982}
4983
4984fn merge_manifest_documents(documents: &[JsonValue]) -> JsonValue {
4985 let version = documents.iter().find(|document| {
4986 document
4987 .get("ManifestType")
4988 .and_then(JsonValue::as_str)
4989 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("version"))
4990 });
4991 let default_locale = documents.iter().find(|document| {
4992 document
4993 .get("ManifestType")
4994 .and_then(JsonValue::as_str)
4995 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("defaultLocale"))
4996 });
4997 let installer = documents.iter().find(|document| {
4998 document
4999 .get("ManifestType")
5000 .and_then(JsonValue::as_str)
5001 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("installer"))
5002 });
5003
5004 let mut singleton = JsonMap::new();
5005 copy_manifest_keys(&mut singleton, version, &["PackageIdentifier", "PackageVersion"]);
5006 copy_all_manifest_keys_except(&mut singleton, default_locale, &["ManifestType", "ManifestVersion"]);
5007 copy_all_manifest_keys_except(&mut singleton, installer, &["ManifestType", "ManifestVersion"]);
5008
5009 if !singleton.contains_key("PackageLocale")
5010 && let Some(package_locale) = default_locale
5011 .and_then(|document| json_string(document, "PackageLocale"))
5012 .or_else(|| version.and_then(|document| json_string(document, "DefaultLocale")))
5013 {
5014 singleton.insert("PackageLocale".to_owned(), JsonValue::String(package_locale));
5015 }
5016
5017 singleton.insert("ManifestType".to_owned(), JsonValue::String("singleton".to_owned()));
5018 singleton.insert(
5019 "ManifestVersion".to_owned(),
5020 JsonValue::String(
5021 installer
5022 .and_then(|document| json_string(document, "ManifestVersion"))
5023 .or_else(|| default_locale.and_then(|document| json_string(document, "ManifestVersion")))
5024 .or_else(|| version.and_then(|document| json_string(document, "ManifestVersion")))
5025 .unwrap_or_else(|| "1.10.0".to_owned()),
5026 ),
5027 );
5028 singleton.remove("DefaultLocale");
5029
5030 JsonValue::Object(singleton)
5031}
5032
5033fn is_merged_manifest_document(document: &JsonValue) -> bool {
5034 document
5035 .get("ManifestType")
5036 .and_then(JsonValue::as_str)
5037 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("merged"))
5038}
5039
5040fn split_merged_manifest_document(merged: &JsonValue) -> Vec<JsonValue> {
5041 let package_identifier = json_string(merged, "PackageIdentifier").unwrap_or_default();
5042 let package_version = json_string(merged, "PackageVersion").unwrap_or_default();
5043 let package_locale = json_string(merged, "PackageLocale").unwrap_or_else(|| "en-US".to_owned());
5044 let manifest_version = json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned());
5045
5046 let version_document = serde_json::json!({
5047 "PackageIdentifier": package_identifier,
5048 "PackageVersion": package_version,
5049 "DefaultLocale": package_locale,
5050 "ManifestType": "version",
5051 "ManifestVersion": manifest_version,
5052 });
5053
5054 let mut default_locale_document = project_manifest_document(
5055 merged,
5056 &[
5057 "PackageIdentifier",
5058 "PackageVersion",
5059 "PackageLocale",
5060 "Publisher",
5061 "PublisherUrl",
5062 "PublisherSupportUrl",
5063 "PrivacyUrl",
5064 "Author",
5065 "PackageName",
5066 "PackageUrl",
5067 "License",
5068 "LicenseUrl",
5069 "Copyright",
5070 "CopyrightUrl",
5071 "ShortDescription",
5072 "Description",
5073 "Moniker",
5074 "Tags",
5075 "Agreements",
5076 "ReleaseNotes",
5077 "ReleaseNotesUrl",
5078 "PurchaseUrl",
5079 "InstallationNotes",
5080 "Documentations",
5081 "Icons",
5082 ],
5083 );
5084 default_locale_document.insert(
5085 "PackageIdentifier".to_owned(),
5086 JsonValue::String(json_string(merged, "PackageIdentifier").unwrap_or_default()),
5087 );
5088 default_locale_document.insert(
5089 "PackageVersion".to_owned(),
5090 JsonValue::String(json_string(merged, "PackageVersion").unwrap_or_default()),
5091 );
5092 default_locale_document.insert(
5093 "PackageLocale".to_owned(),
5094 JsonValue::String(json_string(merged, "PackageLocale").unwrap_or_else(|| "en-US".to_owned())),
5095 );
5096 default_locale_document.insert("ManifestType".to_owned(), JsonValue::String("defaultLocale".to_owned()));
5097 default_locale_document.insert(
5098 "ManifestVersion".to_owned(),
5099 JsonValue::String(json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned())),
5100 );
5101
5102 let mut installer_document = project_manifest_document(
5103 merged,
5104 &[
5105 "PackageIdentifier",
5106 "PackageVersion",
5107 "Channel",
5108 "InstallerLocale",
5109 "Platform",
5110 "MinimumOSVersion",
5111 "InstallerType",
5112 "NestedInstallerType",
5113 "NestedInstallerFiles",
5114 "Scope",
5115 "InstallModes",
5116 "InstallerSwitches",
5117 "InstallerSuccessCodes",
5118 "ExpectedReturnCodes",
5119 "UpgradeBehavior",
5120 "Commands",
5121 "Protocols",
5122 "FileExtensions",
5123 "Dependencies",
5124 "PackageFamilyName",
5125 "ProductCode",
5126 "Capabilities",
5127 "RestrictedCapabilities",
5128 "Markets",
5129 "InstallerAbortsTerminal",
5130 "ReleaseDate",
5131 "InstallLocationRequired",
5132 "RequireExplicitUpgrade",
5133 "DisplayInstallWarnings",
5134 "UnsupportedOSArchitectures",
5135 "UnsupportedArguments",
5136 "AppsAndFeaturesEntries",
5137 "ElevationRequirement",
5138 "InstallationMetadata",
5139 "DownloadCommandProhibited",
5140 "RepairBehavior",
5141 "ArchiveBinariesDependOnPath",
5142 "Authentication",
5143 "Installers",
5144 ],
5145 );
5146 installer_document.insert(
5147 "PackageIdentifier".to_owned(),
5148 JsonValue::String(json_string(merged, "PackageIdentifier").unwrap_or_default()),
5149 );
5150 installer_document.insert(
5151 "PackageVersion".to_owned(),
5152 JsonValue::String(json_string(merged, "PackageVersion").unwrap_or_default()),
5153 );
5154 installer_document.insert("ManifestType".to_owned(), JsonValue::String("installer".to_owned()));
5155 installer_document.insert(
5156 "ManifestVersion".to_owned(),
5157 JsonValue::String(json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned())),
5158 );
5159
5160 vec![
5161 version_document,
5162 JsonValue::Object(default_locale_document),
5163 JsonValue::Object(installer_document),
5164 ]
5165}
5166
5167fn project_manifest_document(source: &JsonValue, keys: &[&str]) -> JsonMap<String, JsonValue> {
5168 let mut result = JsonMap::new();
5169 if let Some(source) = source.as_object() {
5170 for key in keys {
5171 if let Some(value) = source.get(*key) {
5172 result.insert((*key).to_owned(), value.clone());
5173 }
5174 }
5175 }
5176
5177 result
5178}
5179
5180fn copy_manifest_keys(target: &mut JsonMap<String, JsonValue>, source: Option<&JsonValue>, keys: &[&str]) {
5181 if let Some(source) = source.and_then(JsonValue::as_object) {
5182 for key in keys {
5183 if let Some(value) = source.get(*key) {
5184 target.insert((*key).to_owned(), value.clone());
5185 }
5186 }
5187 }
5188}
5189
5190fn copy_all_manifest_keys_except(
5191 target: &mut JsonMap<String, JsonValue>,
5192 source: Option<&JsonValue>,
5193 excluded_keys: &[&str],
5194) {
5195 if let Some(source) = source.and_then(JsonValue::as_object) {
5196 for (key, value) in source {
5197 if !excluded_keys.iter().any(|excluded| key.eq_ignore_ascii_case(excluded)) {
5198 target.insert(key.clone(), value.clone());
5199 }
5200 }
5201 }
5202}
5203
5204fn build_rest_manifest_documents(
5205 data: &JsonValue,
5206 installer_source: &JsonValue,
5207 default_locale: &JsonValue,
5208 package_id: &str,
5209 version: &str,
5210 channel: &str,
5211) -> JsonValue {
5212 let package_identifier = json_string(data, "PackageIdentifier").unwrap_or_else(|| package_id.to_owned());
5213 let package_locale = json_string(default_locale, "PackageLocale").unwrap_or_else(|| "en-US".to_owned());
5214 let manifest_version = json_string(installer_source, "ManifestVersion")
5215 .or_else(|| json_string(default_locale, "ManifestVersion"))
5216 .or_else(|| json_string(data, "ManifestVersion"))
5217 .unwrap_or_else(|| "1.10.0".to_owned());
5218
5219 let mut version_document = JsonMap::new();
5220 version_document.insert(
5221 "PackageIdentifier".to_owned(),
5222 JsonValue::String(package_identifier.clone()),
5223 );
5224 version_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5225 version_document.insert("DefaultLocale".to_owned(), JsonValue::String(package_locale.clone()));
5226 version_document.insert("ManifestType".to_owned(), JsonValue::String("version".to_owned()));
5227 version_document.insert(
5228 "ManifestVersion".to_owned(),
5229 JsonValue::String(manifest_version.clone()),
5230 );
5231
5232 let mut default_locale_document = default_locale.as_object().cloned().unwrap_or_else(JsonMap::new);
5233 default_locale_document.insert(
5234 "PackageIdentifier".to_owned(),
5235 JsonValue::String(package_identifier.clone()),
5236 );
5237 default_locale_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5238 default_locale_document.insert("PackageLocale".to_owned(), JsonValue::String(package_locale));
5239 default_locale_document.insert("ManifestType".to_owned(), JsonValue::String("defaultLocale".to_owned()));
5240 default_locale_document.insert(
5241 "ManifestVersion".to_owned(),
5242 JsonValue::String(manifest_version.clone()),
5243 );
5244
5245 let mut installer_document = installer_source.as_object().cloned().unwrap_or_else(JsonMap::new);
5246 installer_document.remove("DefaultLocale");
5247 installer_document.remove("Locales");
5248 installer_document.insert("PackageIdentifier".to_owned(), JsonValue::String(package_identifier));
5249 installer_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5250 if !channel.is_empty() {
5251 installer_document.insert("Channel".to_owned(), JsonValue::String(channel.to_owned()));
5252 }
5253 installer_document.insert("ManifestType".to_owned(), JsonValue::String("installer".to_owned()));
5254 installer_document.insert("ManifestVersion".to_owned(), JsonValue::String(manifest_version));
5255
5256 let documents = vec![
5257 JsonValue::Object(version_document),
5258 JsonValue::Object(default_locale_document),
5259 JsonValue::Object(installer_document),
5260 ];
5261
5262 merge_manifest_documents(&documents)
5263}
5264
5265fn parse_yaml_installers(root: &YamlMapping) -> Vec<Installer> {
5266 let base = installer_defaults(root);
5267 let base_switches = yaml_installer_switches(root);
5268 if let Some(items) = root.get(YamlValue::from("Installers")).and_then(YamlValue::as_sequence) {
5269 let installers = items
5270 .iter()
5271 .filter_map(YamlValue::as_mapping)
5272 .map(|item| {
5273 let mut merged = base.clone();
5274 merge_yaml_mapping(&mut merged, item);
5275 let switches = yaml_installer_switches(item).with_fallback(&base_switches);
5276 installer_from_yaml(&merged, switches)
5277 })
5278 .collect::<Vec<_>>();
5279 if !installers.is_empty() {
5280 return installers;
5281 }
5282 }
5283
5284 if base.is_empty() {
5285 Vec::new()
5286 } else {
5287 vec![installer_from_yaml(&base, base_switches)]
5288 }
5289}
5290
5291fn installer_defaults(root: &YamlMapping) -> YamlMapping {
5292 let keys = [
5293 "Architecture",
5294 "InstallerType",
5295 "InstallerUrl",
5296 "InstallerSha256",
5297 "ProductCode",
5298 "InstallerLocale",
5299 "Platform",
5300 "MinimumOSVersion",
5301 "Scope",
5302 "ReleaseDate",
5303 "PackageFamilyName",
5304 "UpgradeCode",
5305 "Commands",
5306 ];
5307 let mut defaults = YamlMapping::new();
5308 for key in keys {
5309 if let Some(value) = root.get(YamlValue::from(key)) {
5310 defaults.insert(YamlValue::from(key), value.clone());
5311 }
5312 }
5313 defaults
5314}
5315
5316fn installer_from_yaml(root: &YamlMapping, switches: InstallerSwitches) -> Installer {
5317 Installer {
5318 architecture: yaml_string(root, "Architecture"),
5319 installer_type: yaml_string(root, "InstallerType"),
5320 url: yaml_string(root, "InstallerUrl"),
5321 sha256: yaml_string(root, "InstallerSha256"),
5322 product_code: yaml_string(root, "ProductCode"),
5323 locale: yaml_string(root, "InstallerLocale"),
5324 scope: yaml_string(root, "Scope"),
5325 release_date: yaml_string(root, "ReleaseDate"),
5326 package_family_name: yaml_string(root, "PackageFamilyName"),
5327 upgrade_code: yaml_string(root, "UpgradeCode"),
5328 platforms: yaml_string_list(root, "Platform"),
5329 minimum_os_version: yaml_string(root, "MinimumOSVersion"),
5330 switches,
5331 commands: yaml_string_list(root, "Commands"),
5332 package_dependencies: yaml_package_dependencies(root),
5333 }
5334}
5335
5336fn yaml_installer_switches(root: &YamlMapping) -> InstallerSwitches {
5337 let mapping = root
5338 .get(YamlValue::from("InstallerSwitches"))
5339 .or_else(|| root.get(YamlValue::from("Switches")))
5340 .and_then(YamlValue::as_mapping);
5341 yaml_installer_switches_from_mapping(mapping)
5342}
5343
5344fn yaml_installer_switches_from_mapping(mapping: Option<&YamlMapping>) -> InstallerSwitches {
5345 let string = |key: &str| mapping.and_then(|mapping| mapping.get(YamlValue::from(key)).and_then(yaml_scalar_string));
5346
5347 InstallerSwitches {
5348 silent: string("Silent"),
5349 silent_with_progress: string("SilentWithProgress"),
5350 interactive: string("Interactive"),
5351 custom: string("Custom"),
5352 log: string("Log"),
5353 install_location: string("InstallLocation"),
5354 }
5355}
5356
5357fn yaml_localized_string(root: &YamlMapping, key: &str) -> Option<String> {
5358 yaml_string(root, key).or_else(|| {
5359 root.get(YamlValue::from("DefaultLocale"))
5360 .and_then(YamlValue::as_mapping)
5361 .and_then(|mapping| yaml_string(mapping, key))
5362 })
5363}
5364
5365fn yaml_string_from_root(root: &YamlMapping, key: &str) -> Option<String> {
5366 yaml_string(root, key)
5367}
5368
5369fn yaml_string(root: &YamlMapping, key: &str) -> Option<String> {
5370 root.get(YamlValue::from(key)).and_then(yaml_scalar_string)
5371}
5372
5373fn yaml_scalar_string(value: &YamlValue) -> Option<String> {
5374 match value {
5375 YamlValue::Null => None,
5376 YamlValue::Bool(value) => Some(value.to_string()),
5377 YamlValue::Number(value) => Some(value.to_string()),
5378 YamlValue::String(value) => Some(value.clone()),
5379 YamlValue::Tagged(tagged) => yaml_scalar_string(&tagged.value),
5380 YamlValue::Sequence(_) | YamlValue::Mapping(_) => None,
5381 }
5382}
5383
5384fn yaml_string_list(root: &YamlMapping, key: &str) -> Vec<String> {
5385 root.get(YamlValue::from(key))
5386 .and_then(YamlValue::as_sequence)
5387 .map(|items| items.iter().filter_map(YamlValue::as_str).map(str::to_owned).collect())
5388 .unwrap_or_default()
5389}
5390
5391fn yaml_documentation_list(root: &YamlMapping) -> Vec<Documentation> {
5392 root.get(YamlValue::from("Documentations"))
5393 .and_then(YamlValue::as_sequence)
5394 .map(|items| {
5395 items
5396 .iter()
5397 .filter_map(YamlValue::as_mapping)
5398 .filter_map(|item| {
5399 let url = yaml_string(item, "DocumentUrl")?;
5400 Some(Documentation {
5401 label: yaml_string(item, "DocumentLabel"),
5402 url,
5403 })
5404 })
5405 .collect()
5406 })
5407 .unwrap_or_default()
5408}
5409
5410fn yaml_agreement_list(root: &YamlMapping) -> Vec<PackageAgreement> {
5411 root.get(YamlValue::from("Agreements"))
5412 .and_then(YamlValue::as_sequence)
5413 .map(|items| {
5414 items
5415 .iter()
5416 .filter_map(YamlValue::as_mapping)
5417 .map(|item| PackageAgreement {
5418 label: yaml_string(item, "AgreementLabel"),
5419 text: yaml_string(item, "Agreement"),
5420 url: yaml_string(item, "AgreementUrl"),
5421 })
5422 .collect()
5423 })
5424 .unwrap_or_default()
5425}
5426
5427fn yaml_package_dependencies(root: &YamlMapping) -> Vec<String> {
5428 root.get(YamlValue::from("Dependencies"))
5429 .and_then(YamlValue::as_mapping)
5430 .and_then(|deps| deps.get(YamlValue::from("PackageDependencies")))
5431 .and_then(YamlValue::as_sequence)
5432 .map(|items| {
5433 items
5434 .iter()
5435 .filter_map(YamlValue::as_mapping)
5436 .filter_map(|item| yaml_string(item, "PackageIdentifier"))
5437 .collect()
5438 })
5439 .unwrap_or_default()
5440}
5441
5442fn json_string(value: &JsonValue, key: &str) -> Option<String> {
5443 value.get(key).and_then(JsonValue::as_str).map(str::to_owned)
5444}
5445
5446fn json_string_list(value: &JsonValue, key: &str) -> Vec<String> {
5447 value
5448 .get(key)
5449 .and_then(JsonValue::as_array)
5450 .map(|items| items.iter().filter_map(JsonValue::as_str).map(str::to_owned).collect())
5451 .unwrap_or_default()
5452}
5453
5454fn json_documentation_list(value: &JsonValue) -> Vec<Documentation> {
5455 value
5456 .get("Documentations")
5457 .and_then(JsonValue::as_array)
5458 .map(|items| {
5459 items
5460 .iter()
5461 .filter_map(|item| {
5462 let url = json_string(item, "DocumentUrl")?;
5463 Some(Documentation {
5464 label: json_string(item, "DocumentLabel"),
5465 url,
5466 })
5467 })
5468 .collect()
5469 })
5470 .unwrap_or_default()
5471}
5472
5473fn json_agreement_list(value: &JsonValue) -> Vec<PackageAgreement> {
5474 value
5475 .get("Agreements")
5476 .and_then(JsonValue::as_array)
5477 .map(|items| {
5478 items
5479 .iter()
5480 .map(|item| PackageAgreement {
5481 label: json_string(item, "AgreementLabel"),
5482 text: json_string(item, "Agreement"),
5483 url: json_string(item, "AgreementUrl"),
5484 })
5485 .collect()
5486 })
5487 .unwrap_or_default()
5488}
5489
5490fn json_package_dependencies(value: &JsonValue) -> Vec<String> {
5491 value
5492 .get("Dependencies")
5493 .and_then(JsonValue::as_object)
5494 .and_then(|deps| deps.get("PackageDependencies"))
5495 .and_then(JsonValue::as_array)
5496 .map(|items| {
5497 items
5498 .iter()
5499 .filter_map(|item| json_string(item, "PackageIdentifier"))
5500 .collect()
5501 })
5502 .unwrap_or_default()
5503}
5504
5505fn json_installer_switches(value: &JsonValue) -> InstallerSwitches {
5506 let switches = value
5507 .get("InstallerSwitches")
5508 .or_else(|| value.get("Switches"))
5509 .and_then(JsonValue::as_object);
5510
5511 let string = |key: &str| {
5512 switches
5513 .and_then(|switches| switches.get(key))
5514 .and_then(JsonValue::as_str)
5515 .map(str::to_owned)
5516 };
5517
5518 InstallerSwitches {
5519 silent: string("Silent"),
5520 silent_with_progress: string("SilentWithProgress"),
5521 interactive: string("Interactive"),
5522 custom: string("Custom"),
5523 log: string("Log"),
5524 install_location: string("InstallLocation"),
5525 }
5526}
5527
5528fn merge_yaml_mapping(target: &mut YamlMapping, source: &YamlMapping) {
5529 for (key, value) in source {
5530 target.insert(key.clone(), value.clone());
5531 }
5532}
5533
5534fn extract_zip_member(bytes: &[u8], member_name: &str) -> Result<Vec<u8>> {
5535 let reader = Cursor::new(bytes.to_vec());
5536 let mut archive = ZipArchive::new(reader).context("failed to read zip payload")?;
5537 let mut file = archive
5538 .by_name(member_name)
5539 .with_context(|| format!("zip payload missing {member_name}"))?;
5540 let mut output = Vec::new();
5541 file.read_to_end(&mut output)
5542 .with_context(|| format!("failed to read {member_name} from zip payload"))?;
5543 Ok(output)
5544}
5545
5546fn decompress_mszyml(bytes: &[u8]) -> Result<String> {
5547 if let Ok(output) = decompress_mszyml_payload(if bytes.starts_with(b"CK") { &bytes[2..] } else { bytes }) {
5548 return Ok(output);
5549 }
5550
5551 if !bytes.starts_with(b"CK")
5552 && let Some(offset) = bytes.windows(2).position(|window| window == b"CK")
5553 {
5554 return decompress_mszyml_payload(&bytes[offset + 2..]);
5555 }
5556
5557 decompress_mszyml_payload(bytes).context("failed to decompress MSZIP payload")
5558}
5559
5560fn decompress_mszyml_payload(payload: &[u8]) -> Result<String> {
5561 let mut decoder = flate2::read::DeflateDecoder::new(payload);
5562 let mut output = String::new();
5563 decoder.read_to_string(&mut output)?;
5564 Ok(output)
5565}
5566
5567fn cache_control_max_age(response: &Response) -> u64 {
5568 response
5569 .headers()
5570 .get(reqwest::header::CACHE_CONTROL)
5571 .and_then(|value| value.to_str().ok())
5572 .and_then(|header| {
5573 header.split(',').find_map(|part| {
5574 let trimmed = part.trim();
5575 trimmed
5576 .strip_prefix("max-age=")
5577 .and_then(|value| value.parse::<u64>().ok())
5578 })
5579 })
5580 .unwrap_or(60)
5581}
5582
5583fn choose_contract(server_versions: &[String]) -> Option<&'static str> {
5584 REST_SUPPORTED_CONTRACTS.iter().copied().find(|candidate| {
5585 let (major, minor) = major_minor(candidate);
5586 server_versions.iter().any(|version| {
5587 let (server_major, server_minor) = major_minor(version);
5588 server_major == major && server_minor == minor
5589 })
5590 })
5591}
5592
5593fn major_minor(version: &str) -> (String, String) {
5594 let mut parts = version.split('.');
5595 (
5596 parts.next().unwrap_or_default().to_owned(),
5597 parts.next().unwrap_or_default().to_owned(),
5598 )
5599}
5600
5601fn default_market() -> String {
5602 std::env::var("WINGET_RS_MARKET").unwrap_or_else(|_| DEFAULT_MARKET.to_owned())
5603}
5604
5605fn select_installer(installers: &[Installer], query: &PackageQuery) -> Option<Installer> {
5606 let requested_locale = query.locale.as_deref();
5607 let requested_architecture = query.installer_architecture.as_deref();
5608 let requested_type = query.installer_type.as_deref();
5609 let requested_scope = query.install_scope.as_deref();
5610 let requested_platform = query.platform.as_deref();
5611 let requested_os_version = query.os_version.as_deref();
5612 let system_architecture = current_architecture();
5613 let system_platform = current_platform();
5614 let current_os_version = current_os_version();
5615
5616 installers
5617 .iter()
5618 .enumerate()
5619 .filter(|(_, installer)| {
5620 installer_matches_requested(
5621 installer,
5622 requested_type,
5623 requested_scope,
5624 requested_platform,
5625 requested_os_version,
5626 system_platform,
5627 current_os_version.as_deref(),
5628 )
5629 })
5630 .filter(|(_, installer)| installer_matches_architecture(installer, requested_architecture, system_architecture))
5631 .max_by_key(|(index, installer)| {
5632 (
5633 installer_rank(installer, requested_locale, requested_architecture, system_architecture),
5634 std::cmp::Reverse(*index),
5635 )
5636 })
5637 .map(|(_, installer)| installer)
5638 .cloned()
5639}
5640
5641fn installer_matches_requested(
5642 installer: &Installer,
5643 requested_type: Option<&str>,
5644 requested_scope: Option<&str>,
5645 requested_platform: Option<&str>,
5646 requested_os_version: Option<&str>,
5647 system_platform: Option<&str>,
5648 current_os_version: Option<&str>,
5649) -> bool {
5650 matches_optional_ci(installer.installer_type.as_deref(), requested_type)
5651 && matches_optional_ci(installer.scope.as_deref(), requested_scope)
5652 && installer_matches_platform(installer, requested_platform, system_platform)
5653 && installer_matches_os_version(installer, requested_os_version, current_os_version)
5654}
5655
5656fn installer_matches_platform(
5657 installer: &Installer,
5658 requested_platform: Option<&str>,
5659 system_platform: Option<&str>,
5660) -> bool {
5661 if installer.platforms.is_empty() {
5662 return true;
5663 }
5664
5665 let Some(effective_platform) = requested_platform.or(system_platform) else {
5666 return true;
5667 };
5668
5669 installer
5670 .platforms
5671 .iter()
5672 .any(|platform| platform.eq_ignore_ascii_case(effective_platform))
5673}
5674
5675fn installer_matches_os_version(
5676 installer: &Installer,
5677 requested_os_version: Option<&str>,
5678 current_os_version: Option<&str>,
5679) -> bool {
5680 let Some(minimum_os_version) = installer.minimum_os_version.as_deref() else {
5681 return true;
5682 };
5683
5684 let Some(actual_os_version) = requested_os_version.or(current_os_version) else {
5685 return true;
5686 };
5687
5688 match (
5689 parse_dotted_version(minimum_os_version),
5690 parse_dotted_version(actual_os_version),
5691 ) {
5692 (Some(minimum), Some(actual)) => actual.cmp(&minimum) != Ordering::Less,
5693 _ => minimum_os_version.eq_ignore_ascii_case(actual_os_version),
5694 }
5695}
5696
5697fn installer_matches_architecture(
5698 installer: &Installer,
5699 requested_architecture: Option<&str>,
5700 system_architecture: &str,
5701) -> bool {
5702 let Some(architecture) = installer.architecture.as_deref() else {
5703 return true;
5704 };
5705
5706 let architecture = architecture.to_ascii_lowercase();
5707 if let Some(requested) = requested_architecture {
5708 return architecture.eq_ignore_ascii_case(requested);
5709 }
5710
5711 preferred_architectures(system_architecture)
5712 .iter()
5713 .any(|candidate| architecture.eq_ignore_ascii_case(candidate))
5714}
5715
5716fn installer_rank(
5717 installer: &Installer,
5718 requested_locale: Option<&str>,
5719 requested_architecture: Option<&str>,
5720 system_architecture: &str,
5721) -> (i32, i32, i32) {
5722 let architecture_rank = architecture_rank(
5723 installer.architecture.as_deref(),
5724 requested_architecture.unwrap_or(system_architecture),
5725 requested_architecture.is_some(),
5726 system_architecture,
5727 );
5728 let locale_rank = locale_rank(installer.locale.as_deref(), requested_locale);
5729 let command_rank = if installer.commands.is_empty() { 0 } else { 1 };
5730 (architecture_rank, locale_rank, command_rank)
5731}
5732
5733fn architecture_rank(
5734 installer_architecture: Option<&str>,
5735 preferred_architecture: &str,
5736 strict: bool,
5737 system_architecture: &str,
5738) -> i32 {
5739 let Some(value) = installer_architecture else {
5740 return 0;
5741 };
5742 if value.eq_ignore_ascii_case(preferred_architecture) {
5743 return 5;
5744 }
5745 if value.eq_ignore_ascii_case("neutral") {
5746 return 4;
5747 }
5748 if strict {
5749 return -1;
5750 }
5751
5752 preferred_architectures(system_architecture)
5753 .iter()
5754 .rev()
5755 .position(|candidate| value.eq_ignore_ascii_case(candidate))
5756 .and_then(|index| i32::try_from(index).ok())
5757 .and_then(|index| index.checked_add(1))
5758 .unwrap_or(-1)
5759}
5760
5761fn preferred_architectures(system_architecture: &str) -> &'static [&'static str] {
5762 match system_architecture {
5763 "arm64" => &["arm64", "neutral", "x64", "x86"],
5764 "x64" => &["x64", "neutral", "x86"],
5765 "x86" => &["x86", "neutral"],
5766 _ => &["neutral"],
5767 }
5768}
5769
5770fn locale_rank(installer_locale: Option<&str>, requested_locale: Option<&str>) -> i32 {
5771 match (installer_locale, requested_locale) {
5772 (Some(installer), Some(requested)) if installer.eq_ignore_ascii_case(requested) => 3,
5773 (Some(installer), Some(requested))
5774 if installer
5775 .split('-')
5776 .next()
5777 .zip(requested.split('-').next())
5778 .is_some_and(|(left, right)| left.eq_ignore_ascii_case(right)) =>
5779 {
5780 2
5781 }
5782 (None, Some(_)) => 1,
5783 (Some(_), None) => 1,
5784 (None, None) => 0,
5785 _ => -1,
5786 }
5787}
5788
5789fn matches_optional_ci(value: Option<&str>, requested: Option<&str>) -> bool {
5790 match requested {
5791 Some(requested) => value.is_some_and(|value| value.eq_ignore_ascii_case(requested)),
5792 None => true,
5793 }
5794}
5795
5796fn current_architecture() -> &'static str {
5797 match std::env::consts::ARCH {
5798 "x86_64" => "x64",
5799 "x86" => "x86",
5800 "aarch64" => "arm64",
5801 _ => "neutral",
5802 }
5803}
5804
5805fn current_platform() -> Option<&'static str> {
5806 if cfg!(windows) { Some("Windows.Desktop") } else { None }
5807}
5808
5809#[cfg(windows)]
5810fn current_os_version() -> Option<String> {
5811 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
5812 let key = hklm.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion").ok()?;
5813 let build: String = key.get_value("CurrentBuildNumber").ok()?;
5814 let ubr: u32 = key.get_value("UBR").unwrap_or(0);
5815 let major: u32 = key.get_value("CurrentMajorVersionNumber").unwrap_or(10);
5816 let minor: u32 = key.get_value("CurrentMinorVersionNumber").unwrap_or(0);
5817 Some(format!("{major}.{minor}.{build}.{ubr}"))
5818}
5819
5820#[cfg(not(windows))]
5821fn current_os_version() -> Option<String> {
5822 None
5823}
5824
5825fn parse_dotted_version(version: &str) -> Option<Vec<u64>> {
5826 let mut parsed = Vec::new();
5827 for component in version.split('.') {
5828 if component.is_empty() {
5829 continue;
5830 }
5831 parsed.push(component.parse().ok()?);
5832 }
5833
5834 if parsed.is_empty() { None } else { Some(parsed) }
5835}
5836
5837fn select_v1_version(
5838 versions: &[V1VersionRow],
5839 requested_version: Option<&str>,
5840 requested_channel: Option<&str>,
5841) -> Result<V1VersionRow> {
5842 let mut ordered = versions.to_vec();
5843 ordered.sort_by(|left, right| {
5844 compare_version_and_channel(&right.version, &right.channel, &left.version, &left.channel)
5845 });
5846
5847 if let Some(version) = requested_version {
5848 let channel = requested_channel.unwrap_or_default();
5849 return ordered
5850 .iter()
5851 .find(|candidate| {
5852 candidate.version.eq_ignore_ascii_case(version)
5853 && (channel.is_empty() || candidate.channel.eq_ignore_ascii_case(channel))
5854 })
5855 .cloned()
5856 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5857 }
5858
5859 ordered
5860 .into_iter()
5861 .next()
5862 .ok_or_else(|| anyhow!("package had no versions"))
5863}
5864
5865fn select_v2_version(
5866 versions: &[PackageVersionDataEntry],
5867 requested_version: Option<&str>,
5868) -> Result<PackageVersionDataEntry> {
5869 let mut ordered = versions.to_vec();
5870 ordered.sort_by(|left, right| compare_version(&right.version, &left.version));
5871
5872 if let Some(version) = requested_version {
5873 return ordered
5874 .iter()
5875 .find(|candidate| candidate.version.eq_ignore_ascii_case(version))
5876 .cloned()
5877 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5878 }
5879
5880 ordered
5881 .into_iter()
5882 .next()
5883 .ok_or_else(|| anyhow!("package had no V2 versions"))
5884}
5885
5886fn select_rest_version<'a>(
5887 versions: &'a [VersionKey],
5888 requested_version: Option<&str>,
5889 requested_channel: Option<&str>,
5890) -> Result<&'a VersionKey> {
5891 if let Some(version) = requested_version {
5892 let channel = requested_channel.unwrap_or_default();
5893 return versions
5894 .iter()
5895 .find(|candidate| {
5896 candidate.version.eq_ignore_ascii_case(version)
5897 && (channel.is_empty() || candidate.channel.eq_ignore_ascii_case(channel))
5898 })
5899 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5900 }
5901
5902 versions.first().ok_or_else(|| anyhow!("package had no REST versions"))
5903}
5904
5905fn sort_versions_desc(versions: &mut [VersionKey]) {
5906 versions.sort_by(|left, right| {
5907 compare_version_and_channel(&right.version, &right.channel, &left.version, &left.channel)
5908 });
5909}
5910
5911fn compare_version_and_channel(
5912 left_version: &str,
5913 left_channel: &str,
5914 right_version: &str,
5915 right_channel: &str,
5916) -> Ordering {
5917 compare_version(left_version, right_version).then_with(|| {
5918 left_channel
5919 .to_ascii_lowercase()
5920 .cmp(&right_channel.to_ascii_lowercase())
5921 })
5922}
5923
5924fn compare_version(left: &str, right: &str) -> Ordering {
5925 let left_parts = tokenize_version(left);
5926 let right_parts = tokenize_version(right);
5927 let max_len = left_parts.len().max(right_parts.len());
5928
5929 for index in 0..max_len {
5930 let left_part = left_parts.get(index).map(String::as_str).unwrap_or("0");
5931 let right_part = right_parts.get(index).map(String::as_str).unwrap_or("0");
5932 let numeric = left_part.parse::<u64>().ok().zip(right_part.parse::<u64>().ok());
5933
5934 let ordering = if let Some((left_number, right_number)) = numeric {
5935 left_number.cmp(&right_number)
5936 } else {
5937 left_part.to_ascii_lowercase().cmp(&right_part.to_ascii_lowercase())
5938 };
5939
5940 if ordering != Ordering::Equal {
5941 return ordering;
5942 }
5943 }
5944
5945 Ordering::Equal
5946}
5947
5948fn tokenize_version(value: &str) -> Vec<String> {
5949 let mut parts = Vec::new();
5950 let mut current = String::new();
5951 let mut current_is_digit = None;
5952
5953 for ch in value.chars() {
5954 if ch == '.' || ch == '-' || ch == '_' || ch == '+' {
5955 if !current.is_empty() {
5956 parts.push(std::mem::take(&mut current));
5957 }
5958 current_is_digit = None;
5959 continue;
5960 }
5961
5962 let is_digit = ch.is_ascii_digit();
5963 match current_is_digit {
5964 Some(previous) if previous != is_digit => {
5965 parts.push(std::mem::take(&mut current));
5966 current.push(ch);
5967 current_is_digit = Some(is_digit);
5968 }
5969 Some(_) => current.push(ch),
5970 None => {
5971 current.push(ch);
5972 current_is_digit = Some(is_digit);
5973 }
5974 }
5975 }
5976
5977 if !current.is_empty() {
5978 parts.push(current);
5979 }
5980
5981 parts
5982}
5983
5984fn verify_hash(expected_hash: &str, bytes: &[u8]) -> Result<()> {
5985 if !hash_matches(Some(expected_hash), bytes) {
5986 bail!("downloaded file hash did not match expected SHA256");
5987 }
5988 Ok(())
5989}
5990
5991fn verify_installer_hash(expected_hash: Option<&str>, bytes: &[u8], ignore_security_hash: bool) -> Result<()> {
5992 let Some(expected_hash) = expected_hash else {
5993 return Ok(());
5994 };
5995 if ignore_security_hash {
5996 return Ok(());
5997 }
5998
5999 if !hash_matches(Some(expected_hash), bytes) {
6000 let actual = sha256_hex(bytes);
6001 bail!("Installer hash mismatch. Expected: {expected_hash}, Got: {actual}");
6002 }
6003 Ok(())
6004}
6005
6006fn hash_matches(expected_hash: Option<&str>, bytes: &[u8]) -> bool {
6007 expected_hash
6008 .map(|expected| sha256_hex(bytes).eq_ignore_ascii_case(expected))
6009 .unwrap_or(true)
6010}
6011
6012fn sha256_hex(bytes: &[u8]) -> String {
6013 let digest = Sha256::digest(bytes);
6014 let mut output = String::with_capacity(digest.len() * 2);
6015 for byte in digest {
6016 output.push_str(&format!("{byte:02X}"));
6017 }
6018 output
6019}
6020
6021fn pins_db_path(app_root: &Path) -> PathBuf {
6022 if uses_packaged_layout(app_root) {
6023 app_root.join("pinning.db")
6024 } else {
6025 app_root.join("pins.db")
6026 }
6027}
6028
6029fn resolve_pin_type_column(conn: &Connection) -> Result<Option<&'static str>> {
6030 let mut stmt = conn.prepare("PRAGMA table_info(pin)")?;
6031 let mut has_current_column = false;
6032 let mut has_legacy_column = false;
6033
6034 let column_names = stmt.query_map([], |row| row.get::<_, String>(1))?;
6035 for column_name in column_names {
6036 match column_name?.as_str() {
6037 "type" => has_current_column = true,
6038 "pin_type" => has_legacy_column = true,
6039 _ => {}
6040 }
6041 }
6042
6043 Ok(if has_current_column {
6044 Some("type")
6045 } else if has_legacy_column {
6046 Some("pin_type")
6047 } else {
6048 None
6049 })
6050}
6051
6052fn decode_pin_type(pin_type_int: i64) -> PinType {
6053 match pin_type_int {
6054 4 => PinType::Blocking,
6055 3 => PinType::Gating,
6056 _ => PinType::Pinning,
6057 }
6058}
6059
6060#[cfg(windows)]
6061fn dispatch_installer(
6062 installer_path: &Path,
6063 installer_type: &str,
6064 request: &InstallRequest,
6065 manifest: &Manifest,
6066 installer: &Installer,
6067) -> Result<i32> {
6068 use std::process::Command;
6069
6070 match installer_type {
6071 "msi" | "wix" => {
6072 let mut cmd = Command::new("msiexec");
6073 cmd.args(installer_command_arguments(
6074 installer_type,
6075 request,
6076 manifest,
6077 installer_path,
6078 installer,
6079 ));
6080 let status = cmd.status().context("failed to run msiexec")?;
6081 Ok(status.code().unwrap_or(-1))
6082 }
6083 "msix" | "appx" => {
6084 let mut cmd = Command::new("powershell");
6085 cmd.arg("-NoProfile")
6086 .arg("-Command")
6087 .arg(format!("Add-AppxPackage -Path '{}'", installer_path.display()));
6088 let status = cmd.status().context("failed to run Add-AppxPackage")?;
6089 Ok(status.code().unwrap_or(-1))
6090 }
6091 "zip" => {
6092 let target = dirs::data_local_dir()
6093 .unwrap_or_else(|| PathBuf::from("."))
6094 .join("Programs");
6095 fs::create_dir_all(&target)?;
6096 let file = fs::File::open(installer_path)?;
6097 let mut archive = ZipArchive::new(file)?;
6098 archive.extract(&target)?;
6099 Ok(0)
6100 }
6101 _ => {
6103 let mut cmd = Command::new(installer_path);
6104 cmd.args(installer_command_arguments(
6105 installer_type,
6106 request,
6107 manifest,
6108 installer_path,
6109 installer,
6110 ));
6111 let status = cmd.status().context("failed to run installer")?;
6112 Ok(status.code().unwrap_or(-1))
6113 }
6114 }
6115}
6116
6117#[cfg(not(windows))]
6118fn dispatch_installer(
6119 _installer_path: &Path,
6120 _installer_type: &str,
6121 _request: &InstallRequest,
6122 _manifest: &Manifest,
6123 _installer: &Installer,
6124) -> Result<i32> {
6125 bail!("Installing packages is only supported on Windows")
6126}
6127
6128#[cfg(any(windows, test))]
6129fn installer_command_arguments(
6130 installer_type: &str,
6131 request: &InstallRequest,
6132 _manifest: &Manifest,
6133 installer_path: &Path,
6134 installer: &Installer,
6135) -> Vec<String> {
6136 if let Some(override_args) = request.override_args.as_deref() {
6137 return split_installer_switches(override_args);
6138 }
6139
6140 let mut args = Vec::new();
6141 match installer_type {
6142 "msi" | "wix" => {
6143 args.push("/i".to_owned());
6144 args.push(installer_path.display().to_string());
6145 }
6146 "msix" | "appx" | "zip" => return Vec::new(),
6147 _ => {}
6148 }
6149
6150 let configured = match request.mode {
6151 InstallerMode::Interactive => installer.switches.interactive.as_deref(),
6152 InstallerMode::SilentWithProgress => installer
6153 .switches
6154 .silent_with_progress
6155 .as_deref()
6156 .or(installer.switches.silent.as_deref()),
6157 InstallerMode::Silent => installer
6158 .switches
6159 .silent
6160 .as_deref()
6161 .or(installer.switches.silent_with_progress.as_deref()),
6162 };
6163
6164 if let Some(value) = configured.filter(|value| !value.trim().is_empty()) {
6165 args.extend(split_installer_switches(value));
6166 } else {
6167 args.extend(default_installer_arguments(installer_type, request.mode));
6168 }
6169
6170 append_switches(
6171 &mut args,
6172 resolve_template_switch(
6173 installer.switches.log.as_deref(),
6174 default_log_switch(installer_type),
6175 request.log_path.as_ref().map(|path| path.display().to_string()),
6176 "<LOGPATH>",
6177 ),
6178 );
6179 append_switches(&mut args, installer.switches.custom.clone());
6180 append_switches(&mut args, request.custom.clone());
6181 append_switches(
6182 &mut args,
6183 resolve_template_switch(
6184 installer.switches.install_location.as_deref(),
6185 default_install_location_switch(installer_type),
6186 request.install_location.clone(),
6187 "<INSTALLPATH>",
6188 ),
6189 );
6190 args
6191}
6192
6193#[cfg(any(windows, test))]
6194fn default_installer_arguments(installer_type: &str, mode: InstallerMode) -> Vec<String> {
6195 match mode {
6196 InstallerMode::Interactive => Vec::new(),
6197 InstallerMode::SilentWithProgress => match installer_type {
6198 "inno" => vec![
6199 "/SP-".to_owned(),
6200 "/SILENT".to_owned(),
6201 "/SUPPRESSMSGBOXES".to_owned(),
6202 "/NORESTART".to_owned(),
6203 ],
6204 "burn" | "wix" | "msi" => vec!["/passive".to_owned(), "/norestart".to_owned()],
6205 "nullsoft" | "nsis" => vec!["/S".to_owned()],
6206 _ => vec!["/SILENT".to_owned()],
6207 },
6208 InstallerMode::Silent => match installer_type {
6209 "inno" => vec![
6210 "/SP-".to_owned(),
6211 "/VERYSILENT".to_owned(),
6212 "/SUPPRESSMSGBOXES".to_owned(),
6213 "/NORESTART".to_owned(),
6214 ],
6215 "burn" | "wix" | "msi" => vec!["/quiet".to_owned(), "/norestart".to_owned()],
6216 "nullsoft" | "nsis" => vec!["/S".to_owned()],
6217 _ => vec!["/S".to_owned()],
6218 },
6219 }
6220}
6221
6222#[cfg(any(windows, test))]
6223fn default_log_switch(installer_type: &str) -> Option<&'static str> {
6224 match installer_type {
6225 "burn" | "wix" | "msi" => Some("/log \"<LOGPATH>\""),
6226 "inno" => Some("/LOG=\"<LOGPATH>\""),
6227 _ => None,
6228 }
6229}
6230
6231#[cfg(any(windows, test))]
6232fn default_install_location_switch(installer_type: &str) -> Option<&'static str> {
6233 match installer_type {
6234 "burn" | "wix" | "msi" => Some("TARGETDIR=\"<INSTALLPATH>\""),
6235 "nullsoft" | "nsis" => Some("/D=<INSTALLPATH>"),
6236 "inno" => Some("/DIR=\"<INSTALLPATH>\""),
6237 _ => None,
6238 }
6239}
6240
6241#[cfg(any(windows, test))]
6242fn resolve_template_switch(
6243 manifest_value: Option<&str>,
6244 fallback: Option<&str>,
6245 replacement: Option<String>,
6246 token: &str,
6247) -> Option<String> {
6248 let template = manifest_value.filter(|value| !value.trim().is_empty()).or(fallback)?;
6249 let replacement = replacement?;
6250 Some(template.replace(token, &replacement))
6251}
6252
6253#[cfg(any(windows, test))]
6254fn append_switches(args: &mut Vec<String>, value: Option<String>) {
6255 if let Some(value) = value.filter(|value| !value.trim().is_empty()) {
6256 args.extend(split_installer_switches(&value));
6257 }
6258}
6259
6260#[cfg(any(windows, test))]
6261fn split_installer_switches(value: &str) -> Vec<String> {
6262 let mut args = Vec::new();
6263 let mut current = String::new();
6264 let mut in_quotes = false;
6265
6266 for ch in value.chars() {
6267 match ch {
6268 '"' => in_quotes = !in_quotes,
6269 c if c.is_whitespace() && !in_quotes => {
6270 if !current.is_empty() {
6271 args.push(std::mem::take(&mut current));
6272 }
6273 }
6274 _ => current.push(ch),
6275 }
6276 }
6277
6278 if !current.is_empty() {
6279 args.push(current);
6280 }
6281
6282 args
6283}
6284
6285#[cfg(windows)]
6286fn uninstall_package(installed: &ListMatch, request: &UninstallRequest) -> Result<i32> {
6287 if request.purge && request.preserve {
6288 bail!("--purge and --preserve cannot be used together.");
6289 }
6290
6291 if installed
6292 .installer_category
6293 .as_deref()
6294 .is_some_and(|value| value.eq_ignore_ascii_case("portable"))
6295 {
6296 return uninstall_portable(installed, request);
6297 }
6298
6299 if (request.purge || request.preserve) && !request.force {
6300 bail!("--purge and --preserve are currently only supported for portable packages.");
6301 }
6302
6303 if let Some(exit_code) = try_uninstall_arp(installed, request)? {
6304 return Ok(exit_code);
6305 }
6306
6307 if let Some(exit_code) = try_uninstall_msix(installed)? {
6308 return Ok(exit_code);
6309 }
6310
6311 bail!(
6312 "No uninstall command found for installed package '{}' ({})",
6313 installed.name,
6314 installed.local_id
6315 );
6316}
6317
6318#[cfg(windows)]
6319fn try_uninstall_arp(installed: &ListMatch, request: &UninstallRequest) -> Result<Option<i32>> {
6320 use std::process::Command;
6321
6322 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
6323 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
6324
6325 let arp_paths = [
6326 r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
6327 r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
6328 ];
6329
6330 for root in [&hklm, &hkcu] {
6331 for arp_path in &arp_paths {
6332 if let Ok(key) = root.open_subkey(arp_path) {
6333 for subkey_name in key.enum_keys().filter_map(|k| k.ok()) {
6334 if let Ok(subkey) = key.open_subkey(&subkey_name) {
6335 let display_name: String = subkey.get_value("DisplayName").unwrap_or_default();
6336 let product_code = subkey.get_value::<String, _>("ProductCode").ok();
6337 if !registry_entry_matches_installed_package(
6338 &subkey_name,
6339 &display_name,
6340 product_code.as_deref(),
6341 installed,
6342 ) {
6343 continue;
6344 }
6345
6346 if let Some(exit_code) =
6347 try_run_msi_uninstall(installed, &subkey_name, product_code.as_deref(), request)?
6348 {
6349 return Ok(Some(exit_code));
6350 }
6351
6352 let quiet_uninstall_cmd = subkey.get_value::<String, _>("QuietUninstallString").ok();
6353 let uninstall_cmd: String = if request.mode == InstallerMode::Interactive {
6354 subkey.get_value("UninstallString").ok()
6355 } else {
6356 quiet_uninstall_cmd
6357 .clone()
6358 .or_else(|| subkey.get_value("UninstallString").ok())
6359 }
6360 .context("No uninstall command found in registry")?;
6361
6362 if is_winget_uninstall_command(&uninstall_cmd)
6363 && let Some(exit_code) = try_uninstall_via_winget(installed, request)?
6364 {
6365 return Ok(Some(exit_code));
6366 }
6367
6368 let mut cmd = Command::new("cmd");
6369 let log_path = request.log_path.as_ref().map(|value| value.display().to_string());
6370 cmd.arg("/C").arg(build_uninstall_command_with_mode(
6371 &uninstall_cmd,
6372 request.mode,
6373 quiet_uninstall_cmd.is_some(),
6374 log_path.as_deref(),
6375 ));
6376 let status = cmd.status().context("failed to run uninstaller")?;
6377 return Ok(Some(status.code().unwrap_or(-1)));
6378 }
6379 }
6380 }
6381 }
6382 }
6383
6384 Ok(None)
6385}
6386
6387#[cfg(windows)]
6388fn try_uninstall_msix(installed: &ListMatch) -> Result<Option<i32>> {
6389 use std::process::Command;
6390
6391 if !installed.local_id.starts_with("MSIX\\") && installed.package_family_names.is_empty() {
6392 return Ok(None);
6393 }
6394
6395 let package_full_name = installed.local_id.strip_prefix("MSIX\\");
6396 let mut cmd = Command::new("powershell");
6397 cmd.arg("-NoProfile").arg("-Command").arg(build_msix_uninstall_script(
6398 package_full_name,
6399 &installed.package_family_names,
6400 ));
6401 let status = cmd.status().context("failed to run Remove-AppxPackage")?;
6402 Ok(Some(status.code().unwrap_or(-1)))
6403}
6404
6405#[cfg(windows)]
6406fn arp_subkey_name(local_id: &str) -> Option<&str> {
6407 local_id.strip_prefix("ARP\\")?.splitn(3, '\\').nth(2)
6408}
6409
6410#[cfg(windows)]
6411fn registry_entry_matches_installed_package(
6412 subkey_name: &str,
6413 display_name: &str,
6414 product_code: Option<&str>,
6415 installed: &ListMatch,
6416) -> bool {
6417 if let Some(arp_subkey_name) = arp_subkey_name(&installed.local_id)
6418 && subkey_name.eq_ignore_ascii_case(arp_subkey_name)
6419 {
6420 return true;
6421 }
6422
6423 if installed.product_codes.iter().any(|code| {
6424 code.eq_ignore_ascii_case(subkey_name)
6425 || product_code
6426 .map(|value| code.eq_ignore_ascii_case(value))
6427 .unwrap_or(false)
6428 }) {
6429 return true;
6430 }
6431
6432 display_name.eq_ignore_ascii_case(&installed.name)
6433}
6434
6435#[cfg(all(windows, test))]
6436fn build_uninstall_command(uninstall_cmd: &str, silent: bool, has_quiet_command: bool) -> String {
6437 build_uninstall_command_with_mode(
6438 uninstall_cmd,
6439 if silent {
6440 InstallerMode::Silent
6441 } else {
6442 InstallerMode::Interactive
6443 },
6444 has_quiet_command,
6445 None,
6446 )
6447}
6448
6449#[cfg(windows)]
6450fn build_uninstall_command_with_mode(
6451 uninstall_cmd: &str,
6452 mode: InstallerMode,
6453 has_quiet_command: bool,
6454 log_path: Option<&str>,
6455) -> String {
6456 let uninstall_cmd = log_path
6457 .map(|value| uninstall_cmd.replace("<LOGPATH>", value))
6458 .unwrap_or_else(|| uninstall_cmd.to_owned());
6459 if mode == InstallerMode::Interactive || has_quiet_command {
6460 return uninstall_cmd;
6461 }
6462
6463 let lower = uninstall_cmd.to_ascii_lowercase();
6464 if is_winget_uninstall_command_lower(&lower) {
6465 return uninstall_cmd;
6466 }
6467 if lower.contains("/quiet")
6468 || lower.contains("/passive")
6469 || lower.contains("/verysilent")
6470 || lower.contains("/silent")
6471 || lower.contains(" /s")
6472 {
6473 return uninstall_cmd;
6474 }
6475
6476 format!("{uninstall_cmd} /S")
6477}
6478
6479#[cfg(windows)]
6480fn try_uninstall_via_winget(installed: &ListMatch, request: &UninstallRequest) -> Result<Option<i32>> {
6481 let Some(args) = build_winget_uninstall_arguments(installed, request) else {
6482 return Ok(None);
6483 };
6484
6485 let exit_code = run_winget_uninstall(&args)?;
6486 if exit_code == WINGET_PACKAGE_NOT_FOUND_EXIT_CODE && request.query.install_scope.is_some() {
6487 let Some(args_without_scope) = build_winget_uninstall_arguments_with_scope(installed, request, false) else {
6488 return Ok(Some(exit_code));
6489 };
6490
6491 return Ok(Some(run_winget_uninstall(&args_without_scope)?));
6492 }
6493
6494 Ok(Some(exit_code))
6495}
6496
6497#[cfg(windows)]
6498fn run_winget_uninstall(args: &[String]) -> Result<i32> {
6499 use std::process::Command;
6500
6501 let status = Command::new("winget")
6502 .args(args)
6503 .status()
6504 .context("failed to run delegated winget uninstall")?;
6505 Ok(status.code().unwrap_or(-1))
6506}
6507
6508#[cfg(windows)]
6509fn build_winget_uninstall_arguments(installed: &ListMatch, request: &UninstallRequest) -> Option<Vec<String>> {
6510 build_winget_uninstall_arguments_with_scope(installed, request, true)
6511}
6512
6513#[cfg(windows)]
6514fn build_winget_uninstall_arguments_with_scope(
6515 installed: &ListMatch,
6516 request: &UninstallRequest,
6517 include_scope: bool,
6518) -> Option<Vec<String>> {
6519 if installed.id.eq_ignore_ascii_case(&installed.local_id) {
6520 return None;
6521 }
6522
6523 let mut args = vec![
6524 "uninstall".to_owned(),
6525 "--id".to_owned(),
6526 installed.id.clone(),
6527 "--exact".to_owned(),
6528 "--disable-interactivity".to_owned(),
6529 ];
6530
6531 if let Some(source_name) = installed.source_name.as_deref().filter(|value| !value.is_empty()) {
6532 args.push("--source".to_owned());
6533 args.push(source_name.to_owned());
6534 }
6535
6536 if include_scope && let Some(scope) = request.query.install_scope.as_deref().filter(|value| !value.is_empty()) {
6537 args.push("--scope".to_owned());
6538 args.push(scope.to_owned());
6539 }
6540
6541 if request.mode == InstallerMode::Silent {
6542 args.push("--silent".to_owned());
6543 }
6544
6545 Some(args)
6546}
6547
6548#[cfg(windows)]
6549fn is_winget_uninstall_command(command: &str) -> bool {
6550 is_winget_uninstall_command_lower(&command.to_ascii_lowercase())
6551}
6552
6553#[cfg(windows)]
6554fn is_winget_uninstall_command_lower(command: &str) -> bool {
6555 command.contains("winget uninstall") || command.contains("winget.exe uninstall")
6556}
6557
6558#[cfg(windows)]
6559fn build_msix_uninstall_script(package_full_name: Option<&str>, package_family_names: &[String]) -> String {
6560 let full_name_literal = package_full_name
6561 .map(|value| format!("'{}'", value.replace('\'', "''")))
6562 .unwrap_or_else(|| "$null".to_owned());
6563 let family_names_literal = if package_family_names.is_empty() {
6564 "@()".to_owned()
6565 } else {
6566 format!(
6567 "@({})",
6568 package_family_names
6569 .iter()
6570 .map(|value| format!("'{}'", value.replace('\'', "''")))
6571 .collect::<Vec<_>>()
6572 .join(", ")
6573 )
6574 };
6575
6576 format!(
6577 "$fullName = {full_name_literal}; \
6578 $familyNames = {family_names_literal}; \
6579 $targets = Get-AppxPackage | Where-Object {{ \
6580 (($fullName -ne $null) -and $_.PackageFullName -eq $fullName) -or \
6581 ($familyNames.Count -gt 0 -and ($familyNames -contains $_.PackageFamilyName)) }}; \
6582 if (-not $targets) {{ exit 1 }}; \
6583 $targets | Remove-AppxPackage"
6584 )
6585}
6586
6587#[cfg(windows)]
6588fn try_run_msi_uninstall(
6589 installed: &ListMatch,
6590 subkey_name: &str,
6591 product_code: Option<&str>,
6592 request: &UninstallRequest,
6593) -> Result<Option<i32>> {
6594 use std::process::Command;
6595
6596 let uninstall_code = installed
6597 .product_codes
6598 .iter()
6599 .map(String::as_str)
6600 .chain(product_code)
6601 .chain(Some(subkey_name))
6602 .find(|value| is_product_code_like(value));
6603
6604 let Some(uninstall_code) = uninstall_code else {
6605 return Ok(None);
6606 };
6607
6608 let mut cmd = Command::new("msiexec");
6609 cmd.arg("/x").arg(uninstall_code);
6610 match request.mode {
6611 InstallerMode::Interactive => {}
6612 InstallerMode::SilentWithProgress => {
6613 cmd.arg("/passive").arg("/norestart");
6614 }
6615 InstallerMode::Silent => {
6616 cmd.arg("/quiet").arg("/norestart");
6617 }
6618 }
6619 if let Some(log_path) = &request.log_path {
6620 cmd.arg("/log").arg(log_path);
6621 }
6622
6623 let status = cmd.status().context("failed to run msiexec uninstall")?;
6624 Ok(Some(status.code().unwrap_or(-1)))
6625}
6626
6627#[cfg(windows)]
6628fn uninstall_portable(installed: &ListMatch, request: &UninstallRequest) -> Result<i32> {
6629 let Some(location) = installed.install_location.as_deref() else {
6630 if request.force {
6631 return Ok(0);
6632 }
6633 bail!(
6634 "Portable package '{}' does not expose an install location.",
6635 installed.name
6636 );
6637 };
6638
6639 if request.preserve {
6640 return Ok(0);
6641 }
6642
6643 let path = Path::new(location);
6644 if path.is_dir() {
6645 fs::remove_dir_all(path)?;
6646 return Ok(0);
6647 }
6648 if path.is_file() {
6649 fs::remove_file(path)?;
6650 return Ok(0);
6651 }
6652 if request.force {
6653 return Ok(0);
6654 }
6655
6656 bail!("Portable package location not found: {location}")
6657}
6658
6659#[cfg(windows)]
6660fn is_product_code_like(value: &str) -> bool {
6661 value.starts_with('{') && value.ends_with('}')
6662}
6663
6664#[cfg(not(windows))]
6665fn uninstall_package(_installed: &ListMatch, _request: &UninstallRequest) -> Result<i32> {
6666 bail!("Uninstalling packages is only supported on Windows")
6667}
6668
6669#[cfg(test)]
6670mod tests {
6671 use std::io::Write;
6672 use std::path::{Path, PathBuf};
6673
6674 use flate2::Compression;
6675 use flate2::write::DeflateEncoder;
6676
6677 use super::*;
6678
6679 fn temp_app_root(label: &str) -> PathBuf {
6680 std::env::temp_dir().join(format!(
6681 "pinget-rs-tests-{label}-{}",
6682 Utc::now().timestamp_nanos_opt().unwrap_or_default()
6683 ))
6684 }
6685
6686 #[test]
6687 fn repository_options_capture_custom_host_settings() {
6688 let options =
6689 RepositoryOptions::new(PathBuf::from(r"C:\temp\pinget-test")).with_user_agent("pinget-rs-tests/1.0");
6690
6691 assert_eq!(options.app_root, PathBuf::from(r"C:\temp\pinget-test"));
6692 assert_eq!(options.user_agent, "pinget-rs-tests/1.0");
6693 }
6694
6695 #[test]
6696 fn dependency_install_requests_are_silent_and_not_forced() {
6697 let mut parent = InstallRequest::new(PackageQuery {
6698 source: Some("winget".to_owned()),
6699 ..PackageQuery::default()
6700 });
6701 parent.mode = InstallerMode::SilentWithProgress;
6702 parent.force = true;
6703 parent.accept_package_agreements = true;
6704 parent.ignore_security_hash = true;
6705 parent.dependency_source = Some("dependencies".to_owned());
6706
6707 let dependency =
6708 Repository::create_dependency_install_request("Microsoft.VCRedist.2015+.x64".to_owned(), &parent);
6709
6710 assert_eq!(dependency.query.id.as_deref(), Some("Microsoft.VCRedist.2015+.x64"));
6711 assert_eq!(dependency.query.source.as_deref(), Some("dependencies"));
6712 assert_eq!(dependency.mode, InstallerMode::Silent);
6713 assert!(!dependency.force);
6714 assert!(dependency.no_upgrade);
6715 assert!(dependency.accept_package_agreements);
6716 assert!(dependency.ignore_security_hash);
6717 }
6718
6719 #[test]
6720 fn storage_paths_use_configured_app_root() {
6721 let app_root = PathBuf::from(r"C:\temp\pinget-test");
6722 let source = SourceRecord {
6723 name: "winget/test".to_owned(),
6724 kind: SourceKind::PreIndexed,
6725 arg: "https://example.com/cache".to_owned(),
6726 identifier: "Test.Source".to_owned(),
6727 trust_level: "None".to_owned(),
6728 explicit: false,
6729 priority: 0,
6730 last_update: None,
6731 source_version: None,
6732 };
6733
6734 assert_eq!(store_path(&app_root), app_root.join("sources.json"));
6735 assert_eq!(
6736 source_state_dir(&app_root, &source),
6737 app_root.join("sources").join("winget_test")
6738 );
6739 assert_eq!(pins_db_path(&app_root), app_root.join("pins.db"));
6740 }
6741
6742 #[cfg(windows)]
6743 #[test]
6744 fn packaged_layout_defaults_to_packaged_paths() {
6745 let app_root = default_app_root().expect("default app root");
6746 assert!(uses_packaged_layout(&app_root));
6747 assert!(
6748 user_settings_path(&app_root)
6749 .display()
6750 .to_string()
6751 .ends_with(&format!(r"Packages\{}\LocalState\settings.json", PACKAGED_FAMILY_NAME))
6752 );
6753 assert!(
6754 packaged_file_cache_root(&app_root)
6755 .display()
6756 .to_string()
6757 .ends_with(&format!(
6758 r"Packages\{}\LocalState\Microsoft\Windows Package Manager",
6759 PACKAGED_FAMILY_NAME
6760 ))
6761 );
6762 }
6763
6764 #[test]
6765 fn packaged_source_stream_overlays_defaults_and_metadata() {
6766 let store = parse_packaged_source_store(
6767 Some(
6768 "Sources:\n - Name: winget\n Type: Microsoft.PreIndexed.Package\n Arg: https://cdn.winget.microsoft.com/cache\n Data: Microsoft.Winget.Source_8wekyb3d8bbwe\n IsTombstone: true\n - Name: corp\n Type: Microsoft.Rest\n Arg: https://packages.contoso.test/api\n Data: Contoso.Rest\n Explicit: true\n Priority: 7\n TrustLevel: 1\n IsTombstone: false\n",
6769 ),
6770 Some(
6771 "Sources:\n - Name: corp\n LastUpdate: 1700000000\n SourceVersion: 1.2.3\n",
6772 ),
6773 )
6774 .expect("packaged store");
6775
6776 assert!(!store.sources.iter().any(|source| source.name == "winget"));
6777 let corp = store
6778 .sources
6779 .iter()
6780 .find(|source| source.name == "corp")
6781 .expect("corp source");
6782 assert_eq!(corp.kind, SourceKind::Rest);
6783 assert_eq!(corp.identifier, "Contoso.Rest");
6784 assert_eq!(corp.trust_level, "Trusted");
6785 assert!(corp.explicit);
6786 assert_eq!(corp.priority, 7);
6787 assert_eq!(corp.source_version.as_deref(), Some("1.2.3"));
6788 assert_eq!(corp.last_update, DateTime::<Utc>::from_timestamp(1_700_000_000, 0));
6789 }
6790
6791 #[test]
6792 fn system_winget_export_parses_json_lines() {
6793 let output = r#"{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
6794{"Arg":"https://cdn.contoso.test/cache","Data":"Contoso.Source_8wekyb3d8bbwe","Explicit":true,"Identifier":"Contoso.Source_8wekyb3d8bbwe","Name":"contoso-cache","TrustLevel":["Trusted","StoreOrigin"],"Type":"Microsoft.PreIndexed.Package"}
6795"#;
6796
6797 let sources = parse_system_winget_source_export(output).expect("source export");
6798
6799 assert_eq!(
6800 sources.iter().map(|source| source.name.as_str()).collect::<Vec<_>>(),
6801 vec!["contoso", "contoso-cache"]
6802 );
6803 assert_eq!(sources[0].kind, SourceKind::Rest);
6804 assert_eq!(sources[0].arg, "https://api.contoso.test/feed");
6805 assert_eq!(sources[0].identifier, "api.contoso.test");
6806 assert_eq!(sources[0].trust_level, "Trusted");
6807 assert!(!sources[0].explicit);
6808 assert_eq!(sources[1].kind, SourceKind::PreIndexed);
6809 assert_eq!(sources[1].identifier, "Contoso.Source_8wekyb3d8bbwe");
6810 assert!(sources[1].explicit);
6811 }
6812
6813 #[test]
6814 fn system_winget_export_parses_wrapped_sources() {
6815 let output = r#"{
6816 "Sources": [
6817 {
6818 "Name": "corp",
6819 "Type": "Microsoft.Rest",
6820 "Arg": "https://packages.contoso.test/api",
6821 "Identifier": "Contoso.Rest",
6822 "TrustLevel": "Trusted",
6823 "Explicit": true,
6824 "Priority": 9
6825 }
6826 ]
6827}"#;
6828
6829 let sources = parse_system_winget_source_export(output).expect("source export");
6830 let source = sources.first().expect("source");
6831 assert_eq!(sources.len(), 1);
6832 assert_eq!(source.name, "corp");
6833 assert_eq!(source.kind, SourceKind::Rest);
6834 assert_eq!(source.identifier, "Contoso.Rest");
6835 assert_eq!(source.trust_level, "Trusted");
6836 assert!(source.explicit);
6837 assert_eq!(source.priority, 9);
6838 }
6839
6840 #[test]
6841 fn system_winget_source_store_detects_secure_settings_stub() {
6842 assert!(is_secure_settings_stub(Some(
6843 "SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334"
6844 )));
6845 assert!(!is_secure_settings_stub(Some("Sources:\n - Name: winget\n")));
6846 }
6847
6848 #[test]
6849 fn system_winget_source_store_builds_add_arguments() {
6850 let args = build_winget_source_add_arguments(
6851 "corp",
6852 "https://packages.contoso.test/api",
6853 SourceKind::Rest,
6854 "Trusted",
6855 true,
6856 );
6857
6858 assert_eq!(
6859 args,
6860 vec![
6861 "source",
6862 "add",
6863 "--name",
6864 "corp",
6865 "--arg",
6866 "https://packages.contoso.test/api",
6867 "--type",
6868 "Microsoft.Rest",
6869 "--disable-interactivity",
6870 "--trust-level",
6871 "trusted",
6872 "--explicit",
6873 ]
6874 );
6875 }
6876
6877 fn fake_system_winget_source_export(_args: &[String]) -> Result<WingetSourceCommandResult> {
6878 Ok(WingetSourceCommandResult {
6879 exit_code: 0,
6880 stdout: r#"{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
6881"#
6882 .to_owned(),
6883 stderr: String::new(),
6884 })
6885 }
6886
6887 #[test]
6888 fn packaged_secure_settings_stub_delegates_to_system_winget_export() {
6889 let original_runner = {
6890 let mut runner = SYSTEM_WINGET_SOURCE_COMMAND_RUNNER.write().expect("runner lock");
6891 let original = *runner;
6892 *runner = fake_system_winget_source_export;
6893 original
6894 };
6895
6896 let result = load_packaged_store_from_streams(
6897 Some("SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334"),
6898 None,
6899 );
6900
6901 {
6902 let mut runner = SYSTEM_WINGET_SOURCE_COMMAND_RUNNER.write().expect("runner lock");
6903 *runner = original_runner;
6904 }
6905
6906 let store = result.expect("packaged store").expect("packaged store");
6907 let source = store.sources.first().expect("source");
6908 assert_eq!(store.sources.len(), 1);
6909 assert_eq!(source.name, "contoso");
6910 assert_eq!(source.kind, SourceKind::Rest);
6911 }
6912
6913 #[test]
6914 fn source_metadata_and_named_reset_round_trip() {
6915 let app_root = temp_app_root("source-reset");
6916 let result = (|| -> Result<()> {
6917 let mut repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
6918 repository.add_source_with_metadata(
6919 "test",
6920 "https://example.com/test",
6921 SourceKind::Rest,
6922 Some("trusted"),
6923 true,
6924 4,
6925 )?;
6926
6927 repository.edit_source("test", Some(false), None)?;
6928 let edited = repository
6929 .list_sources()
6930 .into_iter()
6931 .find(|source| source.name == "test")
6932 .expect("source");
6933 assert_eq!(edited.trust_level, "Trusted");
6934 assert!(!edited.explicit);
6935 assert_eq!(edited.priority, 4);
6936
6937 repository.reset_source("test")?;
6938 let reset = repository
6939 .list_sources()
6940 .into_iter()
6941 .find(|source| source.name == "test")
6942 .expect("source");
6943 assert_eq!(reset.trust_level, "Trusted");
6944 assert!(!reset.explicit);
6945 assert_eq!(reset.priority, 4);
6946 assert!(reset.last_update.is_none());
6947 assert!(reset.source_version.is_none());
6948 Ok(())
6949 })();
6950
6951 let _ = fs::remove_dir_all(&app_root);
6952 result.expect("source metadata");
6953 }
6954
6955 #[test]
6956 fn explicit_sources_are_skipped_by_default_search_resolution() {
6957 let app_root = temp_app_root("source-resolution");
6958 let result = (|| -> Result<()> {
6959 let mut repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
6960 repository.add_source_with_metadata(
6961 "explicit-test",
6962 "https://example.com/explicit",
6963 SourceKind::Rest,
6964 Some("trusted"),
6965 true,
6966 10,
6967 )?;
6968
6969 let indexes = repository.resolve_source_indexes(None)?;
6970 let names: Vec<_> = indexes
6971 .into_iter()
6972 .map(|index| repository.list_sources()[index].name.clone())
6973 .collect();
6974 assert!(!names.iter().any(|name| name == "explicit-test"));
6975 Ok(())
6976 })();
6977
6978 let _ = fs::remove_dir_all(&app_root);
6979 result.expect("source resolution");
6980 }
6981
6982 #[test]
6983 fn user_and_admin_settings_round_trip() {
6984 let app_root = temp_app_root("settings");
6985 let result = (|| -> Result<()> {
6986 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
6987 repository.set_user_settings(
6988 &serde_json::json!({
6989 "visual": {
6990 "progressBar": "retro"
6991 }
6992 }),
6993 false,
6994 )?;
6995 repository.set_user_settings(
6996 &serde_json::json!({
6997 "experimentalFeatures": {
6998 "directMSI": true
6999 }
7000 }),
7001 true,
7002 )?;
7003
7004 let user_settings = repository.get_user_settings()?;
7005 assert_eq!(
7006 user_settings["visual"]["progressBar"],
7007 serde_json::Value::String("retro".to_owned())
7008 );
7009 assert_eq!(
7010 user_settings["experimentalFeatures"]["directMSI"],
7011 serde_json::Value::Bool(true)
7012 );
7013 assert!(repository.test_user_settings(
7014 &serde_json::json!({
7015 "experimentalFeatures": {
7016 "directMSI": true
7017 }
7018 }),
7019 true
7020 )?);
7021
7022 repository.set_admin_setting("LocalManifestFiles", true)?;
7023 repository.set_admin_setting("InstallerHashOverride", true)?;
7024 let admin_settings = repository.get_admin_settings()?;
7025 assert_eq!(admin_settings["LocalManifestFiles"], serde_json::Value::Bool(true));
7026 assert_eq!(admin_settings["InstallerHashOverride"], serde_json::Value::Bool(true));
7027
7028 repository.reset_admin_setting(Some("LocalManifestFiles"), false)?;
7029 let reset_one = repository.get_admin_settings()?;
7030 assert_eq!(reset_one["LocalManifestFiles"], serde_json::Value::Bool(false));
7031
7032 repository.reset_admin_setting(None, true)?;
7033 let reset_all = repository.get_admin_settings()?;
7034 for name in Repository::supported_admin_settings() {
7035 assert_eq!(reset_all[*name], serde_json::Value::Bool(false));
7036 }
7037 Ok(())
7038 })();
7039
7040 let _ = fs::remove_dir_all(&app_root);
7041 result.expect("settings");
7042 }
7043
7044 #[test]
7045 fn show_result_structured_document_is_manifest_oriented() {
7046 let result = ShowResult {
7047 package: SearchMatch {
7048 source_name: "winget".to_owned(),
7049 source_kind: SourceKind::PreIndexed,
7050 id: "Test.Package".to_owned(),
7051 name: "Test Package".to_owned(),
7052 moniker: Some("testpkg".to_owned()),
7053 version: Some("1.2.3".to_owned()),
7054 channel: Some("stable".to_owned()),
7055 match_criteria: Some("Id".to_owned()),
7056 },
7057 manifest: Manifest {
7058 id: "Test.Package".to_owned(),
7059 name: "Test Package".to_owned(),
7060 version: "1.2.3".to_owned(),
7061 channel: "stable".to_owned(),
7062 publisher: Some("Contoso".to_owned()),
7063 description: Some("Structured output".to_owned()),
7064 moniker: Some("testpkg".to_owned()),
7065 package_url: None,
7066 publisher_url: None,
7067 publisher_support_url: None,
7068 license: None,
7069 license_url: None,
7070 privacy_url: None,
7071 author: None,
7072 copyright: None,
7073 copyright_url: None,
7074 release_notes: None,
7075 release_notes_url: None,
7076 tags: vec!["utility".to_owned()],
7077 agreements: Vec::new(),
7078 package_dependencies: vec!["Microsoft.VCRedist.2015+.x64".to_owned()],
7079 documentation: vec![Documentation {
7080 label: Some("Docs".to_owned()),
7081 url: "https://example.test/docs".to_owned(),
7082 }],
7083 installers: vec![Installer {
7084 architecture: Some("x64".to_owned()),
7085 installer_type: Some("msix".to_owned()),
7086 url: Some("https://example.test/Test.Package.msix".to_owned()),
7087 sha256: Some("ABC123".to_owned()),
7088 product_code: None,
7089 locale: Some("en-US".to_owned()),
7090 scope: Some("machine".to_owned()),
7091 release_date: None,
7092 package_family_name: None,
7093 upgrade_code: None,
7094 platforms: Vec::new(),
7095 minimum_os_version: None,
7096 switches: InstallerSwitches {
7097 silent: Some("/quiet".to_owned()),
7098 ..InstallerSwitches::default()
7099 },
7100 commands: vec!["testpkg".to_owned()],
7101 package_dependencies: vec!["Microsoft.UI.Xaml.2.8".to_owned()],
7102 }],
7103 },
7104 selected_installer: Some(Installer {
7105 architecture: Some("x64".to_owned()),
7106 installer_type: Some("msix".to_owned()),
7107 url: Some("https://example.test/Test.Package.msix".to_owned()),
7108 sha256: Some("ABC123".to_owned()),
7109 product_code: None,
7110 locale: Some("en-US".to_owned()),
7111 scope: Some("machine".to_owned()),
7112 release_date: None,
7113 package_family_name: None,
7114 upgrade_code: None,
7115 platforms: Vec::new(),
7116 minimum_os_version: None,
7117 switches: InstallerSwitches {
7118 silent: Some("/quiet".to_owned()),
7119 ..InstallerSwitches::default()
7120 },
7121 commands: vec!["testpkg".to_owned()],
7122 package_dependencies: vec!["Microsoft.UI.Xaml.2.8".to_owned()],
7123 }),
7124 cached_files: vec![PathBuf::from(r"C:\temp\cache\Test.Package.yaml")],
7125 warnings: vec!["cache warmed".to_owned()],
7126 manifest_documents: JsonValue::Array(vec![
7127 serde_json::json!({
7128 "PackageIdentifier": "Test.Package",
7129 "PackageVersion": "1.2.3",
7130 "DefaultLocale": "en-US",
7131 "ManifestType": "version",
7132 "ManifestVersion": "1.10.0"
7133 }),
7134 serde_json::json!({
7135 "PackageIdentifier": "Test.Package",
7136 "PackageVersion": "1.2.3",
7137 "PackageLocale": "en-US",
7138 "PackageName": "Test Package",
7139 "Publisher": "Example",
7140 "License": "MIT",
7141 "ShortDescription": "Structured output",
7142 "ManifestType": "defaultLocale",
7143 "ManifestVersion": "1.10.0"
7144 }),
7145 serde_json::json!({
7146 "PackageIdentifier": "Test.Package",
7147 "PackageVersion": "1.2.3",
7148 "ManifestType": "installer",
7149 "ManifestVersion": "1.10.0",
7150 "Installers": [
7151 {
7152 "Architecture": "x64",
7153 "InstallerType": "msix",
7154 "InstallerUrl": "https://example.test/Test.Package.msix",
7155 "InstallerSha256": "ABC123",
7156 "Commands": ["testpkg"],
7157 "InstallerSwitches": { "Silent": "/quiet" },
7158 "Dependencies": {
7159 "PackageDependencies": [
7160 { "PackageIdentifier": "Microsoft.VCRedist.2015+.x64" }
7161 ]
7162 }
7163 }
7164 ]
7165 }),
7166 ]),
7167 };
7168
7169 let document = result.structured_document();
7170
7171 assert_eq!(document["ManifestType"].as_str(), Some("singleton"));
7172 assert_eq!(document["ManifestVersion"].as_str(), Some("1.10.0"));
7173 assert_eq!(document["PackageLocale"].as_str(), Some("en-US"));
7174 assert_eq!(
7175 document["Installers"][0]["Dependencies"]["PackageDependencies"][0]["PackageIdentifier"].as_str(),
7176 Some("Microsoft.VCRedist.2015+.x64")
7177 );
7178 assert_eq!(document["Installers"][0]["Commands"][0].as_str(), Some("testpkg"));
7179 assert_eq!(
7180 document["Installers"][0]["InstallerSwitches"]["Silent"].as_str(),
7181 Some("/quiet")
7182 );
7183 }
7184
7185 #[test]
7186 fn parse_yaml_manifest_bundle_returns_singleton_document() {
7187 let yaml = r#"
7188PackageIdentifier: Test.Package
7189PackageVersion: 1.2.3
7190DefaultLocale: en-US
7191ManifestType: version
7192ManifestVersion: 1.10.0
7193---
7194PackageIdentifier: Test.Package
7195PackageVersion: 1.2.3
7196PackageLocale: en-US
7197PackageName: Test Package
7198Publisher: Example
7199License: MIT
7200ShortDescription: Structured output
7201ManifestType: defaultLocale
7202ManifestVersion: 1.10.0
7203---
7204PackageIdentifier: Test.Package
7205PackageVersion: 1.2.3
7206ManifestType: installer
7207ManifestVersion: 1.10.0
7208Installers:
7209 - Architecture: x64
7210 InstallerType: exe
7211 InstallerUrl: https://example.test/Test.Package.exe
7212 InstallerSha256: ABC123
7213"#;
7214
7215 let (_manifest, documents) = parse_yaml_manifest_bundle(yaml.as_bytes()).expect("bundle");
7216
7217 assert_eq!(documents["ManifestType"].as_str(), Some("singleton"));
7218 assert_eq!(documents["PackageIdentifier"].as_str(), Some("Test.Package"));
7219 assert_eq!(documents["PackageName"].as_str(), Some("Test Package"));
7220 }
7221
7222 #[test]
7223 fn collapse_structured_documents_returns_plural_show_documents() {
7224 let documents = collapse_structured_documents(&[
7225 JsonValue::Array(vec![
7226 serde_json::json!({
7227 "PackageIdentifier": "Test.Package.One",
7228 "PackageVersion": "1.0.0",
7229 "DefaultLocale": "en-US",
7230 "ManifestType": "version",
7231 "ManifestVersion": "1.10.0"
7232 }),
7233 serde_json::json!({
7234 "PackageIdentifier": "Test.Package.One",
7235 "PackageVersion": "1.0.0",
7236 "PackageLocale": "en-US",
7237 "PackageName": "Test Package One",
7238 "ManifestType": "defaultLocale",
7239 "ManifestVersion": "1.10.0"
7240 }),
7241 serde_json::json!({
7242 "PackageIdentifier": "Test.Package.One",
7243 "PackageVersion": "1.0.0",
7244 "ManifestType": "installer",
7245 "ManifestVersion": "1.10.0",
7246 "Installers": [
7247 {
7248 "Architecture": "x64",
7249 "InstallerType": "exe",
7250 "InstallerUrl": "https://example.test/one.exe",
7251 "InstallerSha256": "ABC123"
7252 }
7253 ]
7254 }),
7255 ]),
7256 serde_json::json!({
7257 "PackageIdentifier": "Test.Package.Two",
7258 "PackageVersion": "2.0.0",
7259 "PackageLocale": "en-US",
7260 "PackageName": "Test Package Two",
7261 "ManifestType": "singleton",
7262 "ManifestVersion": "1.12.0"
7263 }),
7264 ]);
7265
7266 assert_eq!(documents.len(), 2);
7267 assert_eq!(documents[0]["ManifestType"].as_str(), Some("singleton"));
7268 assert_eq!(documents[0]["PackageIdentifier"].as_str(), Some("Test.Package.One"));
7269 assert_eq!(documents[0]["PackageName"].as_str(), Some("Test Package One"));
7270 assert_eq!(documents[1]["ManifestType"].as_str(), Some("singleton"));
7271 assert_eq!(documents[1]["PackageIdentifier"].as_str(), Some("Test.Package.Two"));
7272 }
7273
7274 #[test]
7275 fn unsupported_action_result_marks_noop_and_warning() {
7276 let result = unsupported_action_result("Contoso.App", "1.2.3", "install", INSTALL_UNSUPPORTED_WARNING);
7277
7278 assert!(result.success);
7279 assert!(result.no_op);
7280 assert_eq!(result.exit_code, 0);
7281 assert_eq!(result.warnings, vec![INSTALL_UNSUPPORTED_WARNING.to_owned()]);
7282 }
7283
7284 #[test]
7285 fn create_install_no_op_result_honors_no_upgrade() {
7286 let mut request = InstallRequest::new(PackageQuery {
7287 id: Some("Contoso.App".to_owned()),
7288 ..PackageQuery::default()
7289 });
7290 request.no_upgrade = true;
7291
7292 let manifest = Manifest {
7293 id: "Contoso.App".to_owned(),
7294 name: "Contoso App".to_owned(),
7295 version: "2.0.0".to_owned(),
7296 channel: String::new(),
7297 publisher: None,
7298 description: None,
7299 moniker: None,
7300 package_url: None,
7301 publisher_url: None,
7302 publisher_support_url: None,
7303 license: None,
7304 license_url: None,
7305 privacy_url: None,
7306 author: None,
7307 copyright: None,
7308 copyright_url: None,
7309 release_notes: None,
7310 release_notes_url: None,
7311 tags: Vec::new(),
7312 agreements: Vec::new(),
7313 package_dependencies: Vec::new(),
7314 documentation: Vec::new(),
7315 installers: Vec::new(),
7316 };
7317 let existing = ListMatch {
7318 name: "Contoso App".to_owned(),
7319 id: "Contoso.App".to_owned(),
7320 local_id: "Contoso.App".to_owned(),
7321 installed_version: "1.0.0".to_owned(),
7322 available_version: Some("2.0.0".to_owned()),
7323 source_name: Some("winget".to_owned()),
7324 publisher: None,
7325 scope: None,
7326 installer_category: None,
7327 install_location: None,
7328 package_family_names: Vec::new(),
7329 product_codes: Vec::new(),
7330 upgrade_codes: Vec::new(),
7331 };
7332
7333 let result =
7334 Repository::create_install_no_op_result(&request, &manifest, Some(&existing)).expect("no-op result");
7335
7336 assert!(result.success);
7337 assert!(result.no_op);
7338 assert_eq!(result.version, "1.0.0");
7339 assert_eq!(
7340 result.warnings,
7341 vec!["Package is already installed; skipping because --no-upgrade was specified.".to_owned()]
7342 );
7343 }
7344
7345 #[test]
7346 fn create_install_no_op_result_skips_reinstall_when_current() {
7347 let request = InstallRequest::new(PackageQuery {
7348 id: Some("Contoso.App".to_owned()),
7349 ..PackageQuery::default()
7350 });
7351
7352 let manifest = Manifest {
7353 id: "Contoso.App".to_owned(),
7354 name: "Contoso App".to_owned(),
7355 version: "2.0.0".to_owned(),
7356 channel: String::new(),
7357 publisher: None,
7358 description: None,
7359 moniker: None,
7360 package_url: None,
7361 publisher_url: None,
7362 publisher_support_url: None,
7363 license: None,
7364 license_url: None,
7365 privacy_url: None,
7366 author: None,
7367 copyright: None,
7368 copyright_url: None,
7369 release_notes: None,
7370 release_notes_url: None,
7371 tags: Vec::new(),
7372 agreements: Vec::new(),
7373 package_dependencies: Vec::new(),
7374 documentation: Vec::new(),
7375 installers: Vec::new(),
7376 };
7377 let existing = ListMatch {
7378 name: "Contoso App".to_owned(),
7379 id: "Contoso.App".to_owned(),
7380 local_id: "Contoso.App".to_owned(),
7381 installed_version: "2.0.0".to_owned(),
7382 available_version: Some("2.0.0".to_owned()),
7383 source_name: Some("winget".to_owned()),
7384 publisher: None,
7385 scope: None,
7386 installer_category: None,
7387 install_location: None,
7388 package_family_names: Vec::new(),
7389 product_codes: Vec::new(),
7390 upgrade_codes: Vec::new(),
7391 };
7392
7393 let result =
7394 Repository::create_install_no_op_result(&request, &manifest, Some(&existing)).expect("no-op result");
7395
7396 assert!(result.success);
7397 assert!(result.no_op);
7398 assert_eq!(result.version, "2.0.0");
7399 assert_eq!(
7400 result.warnings,
7401 vec!["Package is already installed and up to date; rerun with --force to reinstall.".to_owned()]
7402 );
7403 }
7404
7405 #[test]
7406 fn collapse_structured_document_projects_merged_manifest() {
7407 let document = collapse_structured_document(&serde_json::json!({
7408 "PackageIdentifier": "Test.Package",
7409 "PackageVersion": "1.2.3",
7410 "PackageLocale": "en-US",
7411 "PackageName": "Test Package",
7412 "Publisher": "Example",
7413 "InstallerType": "exe",
7414 "Installers": [
7415 {
7416 "Architecture": "x64",
7417 "InstallerUrl": "https://example.test/Test.Package.exe",
7418 "InstallerSha256": "ABC123"
7419 }
7420 ],
7421 "ManifestType": "merged",
7422 "ManifestVersion": "1.10.0"
7423 }));
7424
7425 assert_eq!(document["ManifestType"].as_str(), Some("singleton"));
7426 assert_eq!(document["PackageIdentifier"].as_str(), Some("Test.Package"));
7427 assert_eq!(document["PackageName"].as_str(), Some("Test Package"));
7428 assert_eq!(document["InstallerType"].as_str(), Some("exe"));
7429 assert_eq!(document["Installers"][0]["Architecture"].as_str(), Some("x64"));
7430 }
7431
7432 #[test]
7433 fn parses_fixture_manifest() {
7434 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
7435 .join("tests")
7436 .join("fixtures")
7437 .join("ManifestV1_28-Singleton.yaml");
7438 let bytes = fs::read(root).expect("fixture bytes");
7439 let manifest = parse_yaml_manifest(&bytes).expect("manifest");
7440 assert_eq!(manifest.id, "microsoft.msixsdk");
7441 assert_eq!(manifest.version, "1.7.32");
7442 assert_eq!(manifest.name, "MSIX SDK");
7443 assert!(!manifest.installers.is_empty());
7444 }
7445
7446 #[test]
7447 fn parses_installer_switches_from_yaml_manifest() {
7448 let yaml = r#"
7449PackageIdentifier: Test.Package
7450PackageVersion: 1.2.3
7451PackageName: Test Package
7452InstallerSwitches:
7453 SilentWithProgress: /SILENT
7454Installers:
7455 - Architecture: x64
7456 InstallerType: inno
7457 InstallerUrl: https://example.test/Test.Package.exe
7458 InstallerSha256: ABC123
7459 InstallerSwitches:
7460 Silent: /VERYSILENT
7461 Interactive: /HELP
7462"#;
7463
7464 let manifest = parse_yaml_manifest(yaml.as_bytes()).expect("manifest");
7465 let installer = &manifest.installers[0];
7466
7467 assert_eq!(installer.switches.silent_with_progress.as_deref(), Some("/SILENT"));
7468 assert_eq!(installer.switches.silent.as_deref(), Some("/VERYSILENT"));
7469 assert_eq!(installer.switches.interactive.as_deref(), Some("/HELP"));
7470 }
7471
7472 #[test]
7473 fn parses_platform_and_minimum_os_version_from_yaml_manifest() {
7474 let yaml = r#"
7475PackageIdentifier: Test.Package
7476PackageVersion: 1.2.3
7477PackageName: Test Package
7478Platform:
7479 - Windows.Desktop
7480MinimumOSVersion: 10.0.19041.0
7481Installers:
7482 - Architecture: x64
7483 InstallerType: exe
7484 InstallerUrl: https://example.test/Test.Package.exe
7485 InstallerSha256: ABC123
7486 - Architecture: x64
7487 InstallerType: msix
7488 Platform:
7489 - Windows.Universal
7490 MinimumOSVersion: 10.0.22621.0
7491 InstallerUrl: https://example.test/Test.Package.msix
7492 InstallerSha256: DEF456
7493"#;
7494
7495 let manifest = parse_yaml_manifest(yaml.as_bytes()).expect("manifest");
7496
7497 assert_eq!(manifest.installers[0].platforms, vec!["Windows.Desktop".to_owned()]);
7498 assert_eq!(
7499 manifest.installers[0].minimum_os_version.as_deref(),
7500 Some("10.0.19041.0")
7501 );
7502 assert_eq!(manifest.installers[1].platforms, vec!["Windows.Universal".to_owned()]);
7503 assert_eq!(
7504 manifest.installers[1].minimum_os_version.as_deref(),
7505 Some("10.0.22621.0")
7506 );
7507 }
7508
7509 #[test]
7510 fn installer_switch_arguments_prefer_manifest_switches() {
7511 let installer = Installer {
7512 architecture: None,
7513 installer_type: Some("inno".to_owned()),
7514 url: None,
7515 sha256: None,
7516 product_code: None,
7517 locale: None,
7518 scope: None,
7519 release_date: None,
7520 package_family_name: None,
7521 upgrade_code: None,
7522 platforms: Vec::new(),
7523 minimum_os_version: None,
7524 switches: InstallerSwitches {
7525 silent: Some("/mysilent".to_owned()),
7526 silent_with_progress: Some("/mysilentwithprogress".to_owned()),
7527 interactive: Some("/myinteractive".to_owned()),
7528 ..InstallerSwitches::default()
7529 },
7530 commands: Vec::new(),
7531 package_dependencies: Vec::new(),
7532 };
7533 let manifest = Manifest {
7534 id: "Test.Package".to_owned(),
7535 name: "Test Package".to_owned(),
7536 version: "1.0.0".to_owned(),
7537 channel: String::new(),
7538 publisher: None,
7539 description: None,
7540 moniker: None,
7541 package_url: None,
7542 publisher_url: None,
7543 publisher_support_url: None,
7544 license: None,
7545 license_url: None,
7546 privacy_url: None,
7547 author: None,
7548 copyright: None,
7549 copyright_url: None,
7550 release_notes: None,
7551 release_notes_url: None,
7552 tags: Vec::new(),
7553 agreements: Vec::new(),
7554 package_dependencies: Vec::new(),
7555 documentation: Vec::new(),
7556 installers: Vec::new(),
7557 };
7558
7559 let mut silent_request = InstallRequest::new(PackageQuery::default());
7560 silent_request.mode = InstallerMode::Silent;
7561 let mut progress_request = InstallRequest::new(PackageQuery::default());
7562 progress_request.mode = InstallerMode::SilentWithProgress;
7563 let mut interactive_request = InstallRequest::new(PackageQuery::default());
7564 interactive_request.mode = InstallerMode::Interactive;
7565 assert_eq!(
7566 installer_command_arguments(
7567 "inno",
7568 &silent_request,
7569 &manifest,
7570 Path::new("installer.exe"),
7571 &installer
7572 ),
7573 vec!["/mysilent".to_owned()]
7574 );
7575 assert_eq!(
7576 installer_command_arguments(
7577 "inno",
7578 &progress_request,
7579 &manifest,
7580 Path::new("installer.exe"),
7581 &installer
7582 ),
7583 vec!["/mysilentwithprogress".to_owned()]
7584 );
7585 assert_eq!(
7586 installer_command_arguments(
7587 "inno",
7588 &interactive_request,
7589 &manifest,
7590 Path::new("installer.exe"),
7591 &installer
7592 ),
7593 vec!["/myinteractive".to_owned()]
7594 );
7595 }
7596
7597 #[test]
7598 fn installer_switch_arguments_use_inno_defaults_without_manifest_switches() {
7599 let installer = Installer {
7600 architecture: None,
7601 installer_type: Some("inno".to_owned()),
7602 url: None,
7603 sha256: None,
7604 product_code: None,
7605 locale: None,
7606 scope: None,
7607 release_date: None,
7608 package_family_name: None,
7609 upgrade_code: None,
7610 platforms: Vec::new(),
7611 minimum_os_version: None,
7612 switches: InstallerSwitches::default(),
7613 commands: Vec::new(),
7614 package_dependencies: Vec::new(),
7615 };
7616 let manifest = Manifest {
7617 id: "Test.Package".to_owned(),
7618 name: "Test Package".to_owned(),
7619 version: "1.0.0".to_owned(),
7620 channel: String::new(),
7621 publisher: None,
7622 description: None,
7623 moniker: None,
7624 package_url: None,
7625 publisher_url: None,
7626 publisher_support_url: None,
7627 license: None,
7628 license_url: None,
7629 privacy_url: None,
7630 author: None,
7631 copyright: None,
7632 copyright_url: None,
7633 release_notes: None,
7634 release_notes_url: None,
7635 tags: Vec::new(),
7636 agreements: Vec::new(),
7637 package_dependencies: Vec::new(),
7638 documentation: Vec::new(),
7639 installers: Vec::new(),
7640 };
7641 let mut progress_request = InstallRequest::new(PackageQuery::default());
7642 progress_request.mode = InstallerMode::SilentWithProgress;
7643 let mut silent_request = InstallRequest::new(PackageQuery::default());
7644 silent_request.mode = InstallerMode::Silent;
7645
7646 assert_eq!(
7647 installer_command_arguments(
7648 "inno",
7649 &progress_request,
7650 &manifest,
7651 Path::new("installer.exe"),
7652 &installer
7653 ),
7654 vec![
7655 "/SP-".to_owned(),
7656 "/SILENT".to_owned(),
7657 "/SUPPRESSMSGBOXES".to_owned(),
7658 "/NORESTART".to_owned()
7659 ]
7660 );
7661 assert_eq!(
7662 installer_command_arguments(
7663 "inno",
7664 &silent_request,
7665 &manifest,
7666 Path::new("installer.exe"),
7667 &installer
7668 ),
7669 vec![
7670 "/SP-".to_owned(),
7671 "/VERYSILENT".to_owned(),
7672 "/SUPPRESSMSGBOXES".to_owned(),
7673 "/NORESTART".to_owned()
7674 ]
7675 );
7676 }
7677
7678 #[test]
7679 fn installer_command_arguments_append_manifest_and_cli_switches() {
7680 let installer = Installer {
7681 architecture: None,
7682 installer_type: Some("msi".to_owned()),
7683 url: None,
7684 sha256: None,
7685 product_code: None,
7686 locale: None,
7687 scope: None,
7688 release_date: None,
7689 package_family_name: None,
7690 upgrade_code: None,
7691 platforms: Vec::new(),
7692 minimum_os_version: None,
7693 switches: InstallerSwitches {
7694 custom: Some("ADDLOCAL=Core".to_owned()),
7695 log: Some("/log \"<LOGPATH>\"".to_owned()),
7696 install_location: Some("TARGETDIR=\"<INSTALLPATH>\"".to_owned()),
7697 ..InstallerSwitches::default()
7698 },
7699 commands: Vec::new(),
7700 package_dependencies: Vec::new(),
7701 };
7702 let manifest = Manifest {
7703 id: "ShareX.ShareX".to_owned(),
7704 name: "ShareX".to_owned(),
7705 version: "19.0.2".to_owned(),
7706 channel: String::new(),
7707 publisher: None,
7708 description: None,
7709 moniker: None,
7710 package_url: None,
7711 publisher_url: None,
7712 publisher_support_url: None,
7713 license: None,
7714 license_url: None,
7715 privacy_url: None,
7716 author: None,
7717 copyright: None,
7718 copyright_url: None,
7719 release_notes: None,
7720 release_notes_url: None,
7721 tags: Vec::new(),
7722 agreements: Vec::new(),
7723 package_dependencies: Vec::new(),
7724 documentation: Vec::new(),
7725 installers: Vec::new(),
7726 };
7727 let mut request = InstallRequest::new(PackageQuery::default());
7728 request.mode = InstallerMode::Silent;
7729 request.log_path = Some(PathBuf::from(r"C:\temp\winget.log"));
7730 request.custom = Some("REBOOT=ReallySuppress".to_owned());
7731 request.install_location = Some(r"C:\Apps\ShareX".to_owned());
7732
7733 assert_eq!(
7734 installer_command_arguments("msi", &request, &manifest, Path::new(r"C:\temp\ShareX.msi"), &installer),
7735 vec![
7736 "/i".to_owned(),
7737 r"C:\temp\ShareX.msi".to_owned(),
7738 "/quiet".to_owned(),
7739 "/norestart".to_owned(),
7740 "/log".to_owned(),
7741 r"C:\temp\winget.log".to_owned(),
7742 "ADDLOCAL=Core".to_owned(),
7743 "REBOOT=ReallySuppress".to_owned(),
7744 r"TARGETDIR=C:\Apps\ShareX".to_owned(),
7745 ]
7746 );
7747 }
7748
7749 #[test]
7750 fn decompresses_mszyml_payload() {
7751 let payload = "sV: 1.0.0\nvD:\n - v: 1.2.3\n rP: manifests/test.yaml\n s256H: ABCD\n";
7752 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
7753 encoder.write_all(payload.as_bytes()).expect("write payload");
7754 let compressed = encoder.finish().expect("finish payload");
7755 let mut mszip = b"CK".to_vec();
7756 mszip.extend_from_slice(&compressed);
7757
7758 let decompressed = decompress_mszyml(&mszip).expect("decompress");
7759 let parsed = serde_yaml::from_str::<PackageVersionDataDocument>(&decompressed).expect("parse");
7760 assert_eq!(parsed.versions[0].version, "1.2.3");
7761 }
7762
7763 #[test]
7764 fn decompresses_mszyml_payload_with_prefix() {
7765 let payload = "sV: 1.0.0\nvD:\n - v: 1.2.3\n rP: manifests/test.yaml\n s256H: ABCD\n";
7766 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
7767 encoder.write_all(payload.as_bytes()).expect("write payload");
7768 let compressed = encoder.finish().expect("finish payload");
7769 let mut prefixed = vec![0u8; 28];
7770 prefixed.extend_from_slice(b"CK");
7771 prefixed.extend_from_slice(&compressed);
7772
7773 let decompressed = decompress_mszyml(&prefixed).expect("decompress");
7774 let parsed = serde_yaml::from_str::<PackageVersionDataDocument>(&decompressed).expect("parse");
7775 assert_eq!(parsed.versions[0].version, "1.2.3");
7776 }
7777
7778 #[test]
7779 fn compares_versions_naturally() {
7780 assert_eq!(compare_version("1.10.0", "1.9.9"), Ordering::Greater);
7781 assert_eq!(compare_version("2.0", "10.0"), Ordering::Less);
7782 assert_eq!(compare_version("1.0.0-preview2", "1.0.0-preview1"), Ordering::Greater);
7783 }
7784
7785 #[test]
7786 fn installer_hash_verification_fails_on_mismatch_without_override() {
7787 let result = verify_installer_hash(Some("DEADBEEF"), b"pinget", false);
7788 assert!(result.is_err());
7789 assert!(
7790 result
7791 .expect_err("hash mismatch")
7792 .to_string()
7793 .contains("Installer hash mismatch")
7794 );
7795 }
7796
7797 #[test]
7798 fn installer_hash_verification_allows_mismatch_with_override() {
7799 verify_installer_hash(Some("DEADBEEF"), b"pinget", true).expect("hash override");
7800 verify_installer_hash(None, b"pinget", false).expect("missing hash");
7801 }
7802
7803 #[test]
7804 fn search_query_many_includes_tag_and_command_conditions() {
7805 let query = PackageQuery {
7806 query: Some("terminal".to_owned()),
7807 ..PackageQuery::default()
7808 };
7809
7810 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Many);
7811
7812 assert!(where_clause.contains("tags2"));
7813 assert!(where_clause.contains("commands2"));
7814 assert_eq!(params.len(), 5);
7815 }
7816
7817 #[test]
7818 fn show_query_single_omits_tag_and_command_conditions() {
7819 let query = PackageQuery {
7820 query: Some("terminal".to_owned()),
7821 ..PackageQuery::default()
7822 };
7823
7824 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Single);
7825
7826 assert!(!where_clause.contains("tags2"));
7827 assert!(!where_clause.contains("commands2"));
7828 assert_eq!(params.len(), 3);
7829 }
7830
7831 #[test]
7832 fn explicit_tag_filter_uses_exact_match() {
7833 let query = PackageQuery {
7834 tag: Some("terminal".to_owned()),
7835 ..PackageQuery::default()
7836 };
7837
7838 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Many);
7839
7840 assert!(where_clause.contains("tags2"));
7841 assert_eq!(params, vec!["terminal".to_owned()]);
7842 }
7843
7844 #[test]
7845 fn rest_tag_filter_uses_exact_match() {
7846 let query = PackageQuery {
7847 tag: Some("terminal".to_owned()),
7848 ..PackageQuery::default()
7849 };
7850 let info = RestInformation {
7851 required_package_match_fields: Vec::new(),
7852 unsupported_package_match_fields: Vec::new(),
7853 required_query_parameters: Vec::new(),
7854 ..RestInformation::default()
7855 };
7856
7857 let body = build_rest_search_body(&query, &info, SearchSemantics::Many).expect("rest body");
7858 let filters = body["Filters"].as_array().expect("filters");
7859
7860 assert_eq!(filters.len(), 1);
7861 assert_eq!(filters[0]["PackageMatchField"], "Tag");
7862 assert_eq!(filters[0]["RequestMatch"]["MatchType"], "Exact");
7863 assert_eq!(filters[0]["RequestMatch"]["KeyWord"], "terminal");
7864 }
7865
7866 #[test]
7867 fn selects_installer_using_requested_filters() {
7868 let installers = vec![
7869 Installer {
7870 architecture: Some("x86".to_owned()),
7871 installer_type: Some("zip".to_owned()),
7872 url: None,
7873 sha256: None,
7874 product_code: None,
7875 locale: Some("en-US".to_owned()),
7876 scope: Some("user".to_owned()),
7877 release_date: None,
7878 package_family_name: None,
7879 upgrade_code: None,
7880 platforms: Vec::new(),
7881 minimum_os_version: None,
7882 switches: InstallerSwitches::default(),
7883 commands: Vec::new(),
7884 package_dependencies: Vec::new(),
7885 },
7886 Installer {
7887 architecture: Some("x64".to_owned()),
7888 installer_type: Some("msix".to_owned()),
7889 url: None,
7890 sha256: None,
7891 product_code: None,
7892 locale: Some("en-US".to_owned()),
7893 scope: Some("user".to_owned()),
7894 release_date: None,
7895 package_family_name: None,
7896 upgrade_code: None,
7897 platforms: Vec::new(),
7898 minimum_os_version: None,
7899 switches: InstallerSwitches::default(),
7900 commands: vec!["demo".to_owned()],
7901 package_dependencies: Vec::new(),
7902 },
7903 ];
7904 let query = PackageQuery {
7905 installer_type: Some("msix".to_owned()),
7906 installer_architecture: Some("x64".to_owned()),
7907 install_scope: Some("user".to_owned()),
7908 locale: Some("en-US".to_owned()),
7909 ..PackageQuery::default()
7910 };
7911
7912 let selected = select_installer(&installers, &query).expect("selected installer");
7913 assert_eq!(selected.installer_type.as_deref(), Some("msix"));
7914 assert_eq!(selected.architecture.as_deref(), Some("x64"));
7915 }
7916
7917 #[test]
7918 fn selects_first_installer_when_rank_ties() {
7919 let installers = vec![
7920 Installer {
7921 architecture: Some("x64".to_owned()),
7922 installer_type: Some("exe".to_owned()),
7923 url: None,
7924 sha256: None,
7925 product_code: None,
7926 locale: Some("en-US".to_owned()),
7927 scope: Some("machine".to_owned()),
7928 release_date: None,
7929 package_family_name: None,
7930 upgrade_code: None,
7931 platforms: Vec::new(),
7932 minimum_os_version: None,
7933 switches: InstallerSwitches::default(),
7934 commands: Vec::new(),
7935 package_dependencies: Vec::new(),
7936 },
7937 Installer {
7938 architecture: Some("x64".to_owned()),
7939 installer_type: Some("exe".to_owned()),
7940 url: None,
7941 sha256: None,
7942 product_code: None,
7943 locale: Some("en-US".to_owned()),
7944 scope: Some("user".to_owned()),
7945 release_date: None,
7946 package_family_name: None,
7947 upgrade_code: None,
7948 platforms: Vec::new(),
7949 minimum_os_version: None,
7950 switches: InstallerSwitches::default(),
7951 commands: Vec::new(),
7952 package_dependencies: Vec::new(),
7953 },
7954 ];
7955
7956 let selected = select_installer(&installers, &PackageQuery::default()).expect("selected installer");
7957 assert_eq!(selected.scope.as_deref(), Some("machine"));
7958 }
7959
7960 #[test]
7961 fn selects_installer_using_requested_platform() {
7962 let installers = vec![
7963 Installer {
7964 architecture: Some("x64".to_owned()),
7965 installer_type: Some("exe".to_owned()),
7966 url: None,
7967 sha256: None,
7968 product_code: None,
7969 locale: Some("en-US".to_owned()),
7970 scope: Some("user".to_owned()),
7971 release_date: None,
7972 package_family_name: None,
7973 upgrade_code: None,
7974 platforms: vec!["Windows.Universal".to_owned()],
7975 minimum_os_version: None,
7976 switches: InstallerSwitches::default(),
7977 commands: Vec::new(),
7978 package_dependencies: Vec::new(),
7979 },
7980 Installer {
7981 architecture: Some("x64".to_owned()),
7982 installer_type: Some("exe".to_owned()),
7983 url: None,
7984 sha256: None,
7985 product_code: None,
7986 locale: Some("en-US".to_owned()),
7987 scope: Some("user".to_owned()),
7988 release_date: None,
7989 package_family_name: None,
7990 upgrade_code: None,
7991 platforms: vec!["Windows.Desktop".to_owned()],
7992 minimum_os_version: None,
7993 switches: InstallerSwitches::default(),
7994 commands: Vec::new(),
7995 package_dependencies: Vec::new(),
7996 },
7997 ];
7998 let query = PackageQuery {
7999 installer_type: Some("exe".to_owned()),
8000 platform: Some("Windows.Desktop".to_owned()),
8001 ..PackageQuery::default()
8002 };
8003
8004 let selected = select_installer(&installers, &query).expect("selected installer");
8005 assert_eq!(selected.platforms, vec!["Windows.Desktop".to_owned()]);
8006 }
8007
8008 #[test]
8009 fn selects_installer_using_requested_os_version() {
8010 let installers = vec![
8011 Installer {
8012 architecture: Some("x64".to_owned()),
8013 installer_type: Some("exe".to_owned()),
8014 url: None,
8015 sha256: None,
8016 product_code: None,
8017 locale: Some("en-US".to_owned()),
8018 scope: Some("user".to_owned()),
8019 release_date: None,
8020 package_family_name: None,
8021 upgrade_code: None,
8022 platforms: Vec::new(),
8023 minimum_os_version: Some("10.0.22621.0".to_owned()),
8024 switches: InstallerSwitches::default(),
8025 commands: Vec::new(),
8026 package_dependencies: Vec::new(),
8027 },
8028 Installer {
8029 architecture: Some("x64".to_owned()),
8030 installer_type: Some("exe".to_owned()),
8031 url: None,
8032 sha256: None,
8033 product_code: None,
8034 locale: Some("en-US".to_owned()),
8035 scope: Some("user".to_owned()),
8036 release_date: None,
8037 package_family_name: None,
8038 upgrade_code: None,
8039 platforms: Vec::new(),
8040 minimum_os_version: Some("10.0.19041.0".to_owned()),
8041 switches: InstallerSwitches::default(),
8042 commands: Vec::new(),
8043 package_dependencies: Vec::new(),
8044 },
8045 ];
8046 let query = PackageQuery {
8047 installer_type: Some("exe".to_owned()),
8048 os_version: Some("10.0.19045.0".to_owned()),
8049 ..PackageQuery::default()
8050 };
8051
8052 let selected = select_installer(&installers, &query).expect("selected installer");
8053 assert_eq!(selected.minimum_os_version.as_deref(), Some("10.0.19041.0"));
8054 }
8055
8056 #[test]
8057 fn derives_correlation_name_candidates() {
8058 let candidates = correlation_name_candidates("PowerToys (Preview) x64");
8059 assert!(candidates.contains(&"PowerToys (Preview) x64".to_owned()));
8060 assert!(candidates.contains(&"PowerToys".to_owned()));
8061 }
8062
8063 #[test]
8064 fn correlates_installed_package_to_available_match() {
8065 let installed = InstalledPackage {
8066 name: "PowerToys (Preview) x64".to_owned(),
8067 local_id: r"ARP\Machine\X64\PowerToys".to_owned(),
8068 installed_version: "0.98.1".to_owned(),
8069 publisher: None,
8070 scope: Some("Machine".to_owned()),
8071 installer_category: Some("exe".to_owned()),
8072 install_location: None,
8073 package_family_names: Vec::new(),
8074 product_codes: Vec::new(),
8075 upgrade_codes: Vec::new(),
8076 correlated: None,
8077 };
8078 let candidates = vec![SearchMatch {
8079 source_name: "winget".to_owned(),
8080 source_kind: SourceKind::PreIndexed,
8081 id: "Microsoft.PowerToys".to_owned(),
8082 name: "PowerToys".to_owned(),
8083 moniker: None,
8084 version: Some("0.98.1".to_owned()),
8085 channel: None,
8086 match_criteria: None,
8087 }];
8088
8089 let correlated = correlate_installed_package(&installed, &candidates, true).expect("correlated");
8090 assert_eq!(correlated.id, "Microsoft.PowerToys");
8091 }
8092
8093 #[test]
8094 fn list_query_uses_available_lookup_for_tag_filters() {
8095 let query = ListQuery {
8096 tag: Some("terminal".to_owned()),
8097 ..ListQuery::default()
8098 };
8099
8100 assert!(list_query_needs_available_lookup(&query));
8101 }
8102
8103 #[test]
8104 fn list_tag_filter_requires_correlated_match() {
8105 let package = InstalledPackage {
8106 name: "PowerToys (Preview) x64".to_owned(),
8107 local_id: r"ARP\Machine\X64\PowerToys".to_owned(),
8108 installed_version: "0.98.1".to_owned(),
8109 publisher: None,
8110 scope: Some("Machine".to_owned()),
8111 installer_category: Some("exe".to_owned()),
8112 install_location: None,
8113 package_family_names: Vec::new(),
8114 product_codes: Vec::new(),
8115 upgrade_codes: Vec::new(),
8116 correlated: None,
8117 };
8118 let query = ListQuery {
8119 tag: Some("powertoys".to_owned()),
8120 ..ListQuery::default()
8121 };
8122
8123 assert!(!list_package_matches(&package, &query));
8124 }
8125
8126 #[test]
8127 fn list_lookup_ignores_display_count() {
8128 let query = ListQuery {
8129 query: Some("git".to_owned()),
8130 count: Some(5),
8131 ..ListQuery::default()
8132 };
8133
8134 let package_query = package_query_from_list_query(&query);
8135 assert_eq!(package_query.count, Some(LIST_LOOKUP_MAX_RESULTS));
8136 }
8137
8138 #[test]
8139 fn strict_list_correlation_avoids_short_substring_matches() {
8140 let installed = InstalledPackage {
8141 name: "GitHub".to_owned(),
8142 local_id: r"ARP\User\X64\GitHub".to_owned(),
8143 installed_version: "1.0.0".to_owned(),
8144 publisher: None,
8145 scope: Some("User".to_owned()),
8146 installer_category: Some("exe".to_owned()),
8147 install_location: None,
8148 package_family_names: Vec::new(),
8149 product_codes: Vec::new(),
8150 upgrade_codes: Vec::new(),
8151 correlated: None,
8152 };
8153 let candidates = vec![SearchMatch {
8154 source_name: "winget".to_owned(),
8155 source_kind: SourceKind::PreIndexed,
8156 id: "Git.Git.PreRelease".to_owned(),
8157 name: "Git".to_owned(),
8158 moniker: None,
8159 version: Some("2.54.0".to_owned()),
8160 channel: None,
8161 match_criteria: None,
8162 }];
8163
8164 assert!(correlate_installed_package(&installed, &candidates, false).is_none());
8165 }
8166
8167 #[test]
8168 fn detects_available_upgrade_from_correlated_package() {
8169 let package = InstalledPackage {
8170 name: "AzCopy v10".to_owned(),
8171 local_id: r"ARP\Machine\X64\AzCopy".to_owned(),
8172 installed_version: "10.32.2".to_owned(),
8173 publisher: None,
8174 scope: Some("Machine".to_owned()),
8175 installer_category: Some("exe".to_owned()),
8176 install_location: None,
8177 package_family_names: Vec::new(),
8178 product_codes: Vec::new(),
8179 upgrade_codes: Vec::new(),
8180 correlated: Some(SearchMatch {
8181 source_name: "winget".to_owned(),
8182 source_kind: SourceKind::PreIndexed,
8183 id: "Microsoft.Azure.AZCopy.10".to_owned(),
8184 name: "AzCopy v10".to_owned(),
8185 moniker: None,
8186 version: Some("10.32.3".to_owned()),
8187 channel: None,
8188 match_criteria: None,
8189 }),
8190 };
8191
8192 assert!(installed_package_has_upgrade(&package));
8193 }
8194
8195 #[test]
8196 fn include_unknown_treats_unknown_version_as_upgradable_when_correlated() {
8197 let package = InstalledPackage {
8198 name: "Example Tool".to_owned(),
8199 local_id: r"ARP\User\X64\ExampleTool".to_owned(),
8200 installed_version: "Unknown".to_owned(),
8201 publisher: None,
8202 scope: Some("User".to_owned()),
8203 installer_category: Some("exe".to_owned()),
8204 install_location: None,
8205 package_family_names: Vec::new(),
8206 product_codes: Vec::new(),
8207 upgrade_codes: Vec::new(),
8208 correlated: Some(SearchMatch {
8209 source_name: "winget".to_owned(),
8210 source_kind: SourceKind::PreIndexed,
8211 id: "Contoso.ExampleTool".to_owned(),
8212 name: "Example Tool".to_owned(),
8213 moniker: None,
8214 version: Some("2.0.0".to_owned()),
8215 channel: None,
8216 match_criteria: None,
8217 }),
8218 };
8219 let query = ListQuery {
8220 upgrade_only: true,
8221 include_unknown: true,
8222 ..Default::default()
8223 };
8224
8225 assert!(installed_package_matches_upgrade_filter(&package, &query));
8226 assert_eq!(
8227 list_match_from_installed(package).available_version.as_deref(),
8228 Some("2.0.0")
8229 );
8230 }
8231
8232 #[test]
8233 fn create_repair_list_query_includes_installed_selectors() {
8234 let request = RepairRequest {
8235 query: PackageQuery {
8236 query: Some("powertoys".to_owned()),
8237 id: Some("Microsoft.PowerToys".to_owned()),
8238 name: Some("PowerToys".to_owned()),
8239 moniker: Some("powertoys".to_owned()),
8240 source: Some("winget".to_owned()),
8241 exact: true,
8242 version: Some("0.98.1".to_owned()),
8243 install_scope: Some("Machine".to_owned()),
8244 ..PackageQuery::default()
8245 },
8246 manifest_path: None,
8247 product_code: Some("{GUID}".to_owned()),
8248 mode: InstallerMode::Silent,
8249 log_path: Some(PathBuf::from("repair.log")),
8250 accept_package_agreements: true,
8251 force: true,
8252 ignore_security_hash: true,
8253 };
8254
8255 let query = Repository::create_repair_list_query(&request);
8256 assert_eq!(query.query.as_deref(), Some("powertoys"));
8257 assert_eq!(query.id.as_deref(), Some("Microsoft.PowerToys"));
8258 assert_eq!(query.name.as_deref(), Some("PowerToys"));
8259 assert_eq!(query.moniker.as_deref(), Some("powertoys"));
8260 assert_eq!(query.product_code.as_deref(), Some("{GUID}"));
8261 assert_eq!(query.version.as_deref(), Some("0.98.1"));
8262 assert_eq!(query.source.as_deref(), Some("winget"));
8263 assert!(query.exact);
8264 assert_eq!(query.install_scope.as_deref(), Some("Machine"));
8265 assert_eq!(query.count, Some(100));
8266 }
8267
8268 #[test]
8269 fn create_repair_install_request_forces_reinstall_of_resolved_installed_package() {
8270 let request = RepairRequest {
8271 query: PackageQuery {
8272 name: Some("PowerToys".to_owned()),
8273 source: Some("winget".to_owned()),
8274 locale: Some("en-US".to_owned()),
8275 installer_architecture: Some("x64".to_owned()),
8276 install_scope: Some("Machine".to_owned()),
8277 ..PackageQuery::default()
8278 },
8279 manifest_path: None,
8280 product_code: None,
8281 mode: InstallerMode::SilentWithProgress,
8282 log_path: Some(PathBuf::from("repair.log")),
8283 accept_package_agreements: true,
8284 force: false,
8285 ignore_security_hash: true,
8286 };
8287 let installed = ListMatch {
8288 name: "PowerToys".to_owned(),
8289 id: "Microsoft.PowerToys".to_owned(),
8290 local_id: r"ARP\Machine\X64\Microsoft.PowerToys".to_owned(),
8291 installed_version: "0.98.1".to_owned(),
8292 available_version: Some("0.98.2".to_owned()),
8293 source_name: Some("winget".to_owned()),
8294 publisher: None,
8295 scope: Some("Machine".to_owned()),
8296 installer_category: Some("exe".to_owned()),
8297 install_location: None,
8298 package_family_names: Vec::new(),
8299 product_codes: Vec::new(),
8300 upgrade_codes: Vec::new(),
8301 };
8302
8303 let install_request = Repository::create_repair_install_request(&request, Some(&installed));
8304 assert_eq!(install_request.query.id.as_deref(), Some("Microsoft.PowerToys"));
8305 assert!(install_request.query.query.is_none());
8306 assert!(install_request.query.name.is_none());
8307 assert!(install_request.query.moniker.is_none());
8308 assert_eq!(install_request.query.source.as_deref(), Some("winget"));
8309 assert_eq!(install_request.query.version.as_deref(), Some("0.98.1"));
8310 assert_eq!(install_request.query.locale.as_deref(), Some("en-US"));
8311 assert_eq!(install_request.query.installer_architecture.as_deref(), Some("x64"));
8312 assert_eq!(install_request.query.install_scope.as_deref(), Some("Machine"));
8313 assert!(install_request.query.exact);
8314 assert_eq!(install_request.mode, InstallerMode::SilentWithProgress);
8315 assert_eq!(install_request.log_path, Some(PathBuf::from("repair.log")));
8316 assert!(install_request.accept_package_agreements);
8317 assert!(install_request.force);
8318 assert!(install_request.ignore_security_hash);
8319 }
8320
8321 #[test]
8322 fn source_scoped_pin_operations_round_trip() {
8323 let app_root = temp_app_root("pins");
8324 let result = (|| -> Result<()> {
8325 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
8326 repository.add_pin("Contoso.Tool", "1.0", "winget", PinType::Pinning)?;
8327 repository.add_pin("Contoso.Tool", "*", "test", PinType::Blocking)?;
8328 repository.add_pin("Fabrikam.Tool", "*", "", PinType::Pinning)?;
8329
8330 assert_eq!(repository.list_pins(None)?.len(), 3);
8331 assert_eq!(repository.list_pins(Some("winget"))?.len(), 1);
8332 assert_eq!(repository.list_pins(Some("test"))?.len(), 1);
8333
8334 assert!(repository.remove_pin("Contoso.Tool", Some("winget"))?);
8335 assert_eq!(repository.list_pins(None)?.len(), 2);
8336 assert!(!repository.remove_pin("Contoso.Tool", Some("winget"))?);
8337
8338 repository.reset_pins(Some("test"))?;
8339 let remaining = repository.list_pins(None)?;
8340 assert_eq!(remaining.len(), 1);
8341 assert_eq!(remaining[0].package_id, "Fabrikam.Tool");
8342
8343 repository.reset_pins(None)?;
8344 assert!(repository.list_pins(None)?.is_empty());
8345 Ok(())
8346 })();
8347
8348 let _ = fs::remove_dir_all(&app_root);
8349 result.expect("pin round trip");
8350 }
8351
8352 #[test]
8353 fn pin_operations_work_with_packaged_pin_schema() {
8354 let app_root = temp_app_root("pins_packaged_schema");
8355 let result = (|| -> Result<()> {
8356 let db_path = pins_db_path(&app_root);
8357 if let Some(parent) = db_path.parent() {
8358 fs::create_dir_all(parent)?;
8359 }
8360
8361 let conn = Connection::open(&db_path)?;
8362 conn.execute_batch(
8363 "CREATE TABLE pin (
8364 package_id TEXT NOT NULL,
8365 source_id TEXT NOT NULL,
8366 type INTEGER NOT NULL,
8367 version TEXT NOT NULL,
8368 PRIMARY KEY (package_id, source_id)
8369 )",
8370 )?;
8371 drop(conn);
8372
8373 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
8374 repository.add_pin("Contoso.Tool", "1.2.*", "winget", PinType::Gating)?;
8375
8376 let pins = repository.list_pins(None)?;
8377 assert_eq!(pins.len(), 1);
8378 assert_eq!(pins[0].package_id, "Contoso.Tool");
8379 assert_eq!(pins[0].version, "1.2.*");
8380 assert_eq!(pins[0].source_id, "winget");
8381 assert_eq!(pins[0].pin_type, PinType::Gating);
8382
8383 let conn = Connection::open(&db_path)?;
8384 let stored_pin = conn.query_row(
8385 "SELECT type, version FROM pin WHERE package_id = ?1 AND source_id = ?2",
8386 rusqlite::params!["Contoso.Tool", "winget"],
8387 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
8388 )?;
8389 assert_eq!(stored_pin.0, 3);
8390 assert_eq!(stored_pin.1, "1.2.*");
8391
8392 Ok(())
8393 })();
8394
8395 let _ = fs::remove_dir_all(&app_root);
8396 result.expect("packaged pin schema round trip");
8397 }
8398
8399 #[test]
8400 fn version_pin_patterns_match_exact_and_prefix_values() {
8401 assert!(version_matches_pin_pattern("1.2.3", "*"));
8402 assert!(version_matches_pin_pattern("1.2.3", "1.2.3"));
8403 assert!(version_matches_pin_pattern("1.2.3", "1.2.*"));
8404 assert!(!version_matches_pin_pattern("1.3.0", "1.2.*"));
8405 assert!(!version_matches_pin_pattern("1.2.3", "2.0.0"));
8406 }
8407
8408 #[test]
8409 fn source_specific_pins_override_source_agnostic_pins() {
8410 let item = ListMatch {
8411 name: "Contoso Tool".to_owned(),
8412 id: "Contoso.Tool".to_owned(),
8413 local_id: r"ARP\User\X64\Contoso.Tool".to_owned(),
8414 installed_version: "1.0.0".to_owned(),
8415 available_version: Some("2.0.0".to_owned()),
8416 source_name: Some("winget".to_owned()),
8417 publisher: None,
8418 scope: None,
8419 installer_category: None,
8420 install_location: None,
8421 package_family_names: Vec::new(),
8422 product_codes: Vec::new(),
8423 upgrade_codes: Vec::new(),
8424 };
8425 let pins = vec![
8426 PinRecord {
8427 package_id: "Contoso.Tool".to_owned(),
8428 version: "*".to_owned(),
8429 source_id: String::new(),
8430 pin_type: PinType::Blocking,
8431 },
8432 PinRecord {
8433 package_id: "Contoso.Tool".to_owned(),
8434 version: "2.0.*".to_owned(),
8435 source_id: "winget".to_owned(),
8436 pin_type: PinType::Pinning,
8437 },
8438 ];
8439
8440 assert!(!is_upgrade_blocked_by_pin(&item, &pins));
8441 }
8442
8443 #[test]
8444 fn blocking_pin_blocks_upgrade() {
8445 let item = ListMatch {
8446 name: "Contoso Tool".to_owned(),
8447 id: "Contoso.Tool".to_owned(),
8448 local_id: r"ARP\User\X64\Contoso.Tool".to_owned(),
8449 installed_version: "1.0.0".to_owned(),
8450 available_version: Some("2.0.0".to_owned()),
8451 source_name: Some("winget".to_owned()),
8452 publisher: None,
8453 scope: None,
8454 installer_category: None,
8455 install_location: None,
8456 package_family_names: Vec::new(),
8457 product_codes: Vec::new(),
8458 upgrade_codes: Vec::new(),
8459 };
8460 let pins = vec![PinRecord {
8461 package_id: "Contoso.Tool".to_owned(),
8462 version: "*".to_owned(),
8463 source_id: "winget".to_owned(),
8464 pin_type: PinType::Blocking,
8465 }];
8466
8467 assert!(is_upgrade_blocked_by_pin(&item, &pins));
8468 }
8469
8470 #[cfg(windows)]
8471 #[test]
8472 fn parses_msix_package_full_name_into_version_and_family() {
8473 let parsed = parse_msix_package_full_name("Microsoft.PowerToys.SparseApp_0.98.1.0_neutral__8wekyb3d8bbwe")
8474 .expect("package metadata");
8475
8476 assert_eq!(parsed.version, "0.98.1.0");
8477 assert_eq!(parsed.family_name, "Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe");
8478 }
8479
8480 #[cfg(windows)]
8481 #[test]
8482 fn recognizes_windows_system_paths() {
8483 assert!(is_windows_system_path(r"C:\Windows\SystemApps\Contoso"));
8484 assert!(!is_windows_system_path(
8485 r"C:\Users\mamoreau\AppData\Local\PowerToys\WinUI3Apps"
8486 ));
8487 }
8488
8489 #[test]
8490 fn search_ranking_prefers_exact_name_over_tag_match() {
8491 let query = PackageQuery {
8492 query: Some("PowerToys".to_owned()),
8493 ..Default::default()
8494 };
8495 let exact_name = SearchMatch {
8496 source_name: "winget".to_owned(),
8497 source_kind: SourceKind::PreIndexed,
8498 id: "Microsoft.PowerToys".to_owned(),
8499 name: "PowerToys".to_owned(),
8500 moniker: Some("powertoys".to_owned()),
8501 version: Some("0.98.1".to_owned()),
8502 channel: None,
8503 match_criteria: Some("Moniker: powertoys".to_owned()),
8504 };
8505 let tag_match = SearchMatch {
8506 source_name: "winget".to_owned(),
8507 source_kind: SourceKind::PreIndexed,
8508 id: "JiriPolasek.QRCodesforCommandPalette".to_owned(),
8509 name: "QR Codes for Command Palette".to_owned(),
8510 moniker: None,
8511 version: Some("0.4.0.0".to_owned()),
8512 channel: None,
8513 match_criteria: Some("Tag: microsoft-powertoys".to_owned()),
8514 };
8515
8516 assert!(search_match_sort_score(&exact_name, &query) > search_match_sort_score(&tag_match, &query));
8517 }
8518
8519 #[test]
8520 fn select_best_text_match_prefers_exact_tag_value() {
8521 let values = vec![
8522 "microsoft-powertoys".to_owned(),
8523 "powertoys-run".to_owned(),
8524 "powertoys".to_owned(),
8525 ];
8526
8527 assert_eq!(
8528 select_best_text_match(values, "PowerToys", false).as_deref(),
8529 Some("powertoys")
8530 );
8531 }
8532
8533 #[test]
8534 fn search_match_criteria_is_blank_for_name_match() {
8535 let query = PackageQuery {
8536 query: Some("PowerToys".to_owned()),
8537 ..Default::default()
8538 };
8539
8540 let result = infer_match_criteria(
8541 "Microsoft.PowerToys",
8542 "PowerToys",
8543 Some("powertoys"),
8544 &query,
8545 SearchSemantics::Many,
8546 |_| Ok(Some("powertoys".to_owned())),
8547 |_| Ok(None),
8548 )
8549 .expect("match criteria");
8550
8551 assert_eq!(result, None);
8552 }
8553
8554 #[test]
8555 fn search_ranking_prefers_direct_name_match_even_with_unknown_version() {
8556 let query = PackageQuery {
8557 query: Some("PowerToys".to_owned()),
8558 ..Default::default()
8559 };
8560 let unknown_version = SearchMatch {
8561 source_name: "msstore".to_owned(),
8562 source_kind: SourceKind::Rest,
8563 id: "XP89DCGQ3K6VLD".to_owned(),
8564 name: "Microsoft PowerToys".to_owned(),
8565 moniker: None,
8566 version: Some("Unknown".to_owned()),
8567 channel: None,
8568 match_criteria: None,
8569 };
8570 let tag_match = SearchMatch {
8571 source_name: "winget".to_owned(),
8572 source_kind: SourceKind::PreIndexed,
8573 id: "Riri.QRCodeforCmdPal".to_owned(),
8574 name: "QR Code for CmdPal".to_owned(),
8575 moniker: None,
8576 version: Some("0.0.1.0".to_owned()),
8577 channel: None,
8578 match_criteria: Some("Tag: powertoys".to_owned()),
8579 };
8580
8581 assert!(search_match_sort_score(&unknown_version, &query) > search_match_sort_score(&tag_match, &query));
8582 }
8583
8584 #[test]
8585 fn search_source_fetch_results_ignore_small_display_count() {
8586 let query = PackageQuery {
8587 count: Some(5),
8588 ..Default::default()
8589 };
8590
8591 assert_eq!(source_fetch_results(&query, SearchSemantics::Many), 50);
8592 assert_eq!(source_fetch_results(&query, SearchSemantics::Single), 5);
8593 }
8594
8595 #[test]
8596 fn search_ranking_prefers_direct_name_prefix_over_tag_only_match() {
8597 let query = PackageQuery {
8598 query: Some("PowerToys".to_owned()),
8599 ..Default::default()
8600 };
8601 let prefix_name = SearchMatch {
8602 source_name: "winget".to_owned(),
8603 source_kind: SourceKind::PreIndexed,
8604 id: "advaith.CurrencyConverterPowerToys".to_owned(),
8605 name: "PowerToys-Run-Currency-Converter".to_owned(),
8606 moniker: None,
8607 version: Some("1.5.4".to_owned()),
8608 channel: None,
8609 match_criteria: None,
8610 };
8611 let exact_tag = SearchMatch {
8612 source_name: "winget".to_owned(),
8613 source_kind: SourceKind::PreIndexed,
8614 id: "Riri.QRCodeforCmdPal".to_owned(),
8615 name: "QR Code for CmdPal".to_owned(),
8616 moniker: None,
8617 version: Some("0.0.1.0".to_owned()),
8618 channel: None,
8619 match_criteria: Some("Tag: powertoys".to_owned()),
8620 };
8621
8622 assert!(search_match_sort_score(&prefix_name, &query) > search_match_sort_score(&exact_tag, &query));
8623 }
8624
8625 #[test]
8626 fn list_sort_prefers_main_package_then_sparse_app() {
8627 let main = InstalledPackage {
8628 name: "PowerToys (Preview) x64".to_owned(),
8629 local_id: r"ARP\User\X64\PowerToys".to_owned(),
8630 installed_version: "0.98.1".to_owned(),
8631 publisher: None,
8632 scope: Some("User".to_owned()),
8633 installer_category: Some("exe".to_owned()),
8634 install_location: None,
8635 package_family_names: Vec::new(),
8636 product_codes: Vec::new(),
8637 upgrade_codes: Vec::new(),
8638 correlated: None,
8639 };
8640 let sparse = InstalledPackage {
8641 name: "PowerToys.SparseApp".to_owned(),
8642 local_id: r"MSIX\Microsoft.PowerToys.SparseApp_0.98.1.0_neutral__8wekyb3d8bbwe".to_owned(),
8643 installed_version: "0.98.1.0".to_owned(),
8644 publisher: None,
8645 scope: Some("User".to_owned()),
8646 installer_category: Some("msix".to_owned()),
8647 install_location: None,
8648 package_family_names: Vec::new(),
8649 product_codes: Vec::new(),
8650 upgrade_codes: Vec::new(),
8651 correlated: None,
8652 };
8653 let extension = InstalledPackage {
8654 name: "PowerToys FileLocksmith Context Menu".to_owned(),
8655 local_id: r"MSIX\Microsoft.PowerToys.FileLocksmithContextMenu_0.98.1.0_neutral__8wekyb3d8bbwe".to_owned(),
8656 installed_version: "0.98.1.0".to_owned(),
8657 publisher: None,
8658 scope: Some("User".to_owned()),
8659 installer_category: Some("msix".to_owned()),
8660 install_location: None,
8661 package_family_names: Vec::new(),
8662 product_codes: Vec::new(),
8663 upgrade_codes: Vec::new(),
8664 correlated: None,
8665 };
8666
8667 assert!(list_sort_weight(&main) < list_sort_weight(&sparse));
8668 assert!(list_sort_weight(&sparse) < list_sort_weight(&extension));
8669 }
8670
8671 #[test]
8672 #[cfg(windows)]
8673 fn arp_subkey_name_extracts_registry_subkey() {
8674 assert_eq!(Some("ShareX"), arp_subkey_name(r"ARP\Machine\X64\ShareX"));
8675 assert_eq!(None, arp_subkey_name(r"MSIX\ShareX_19.0.2_x64__name"));
8676 }
8677
8678 #[test]
8679 #[cfg(windows)]
8680 fn registry_entry_matching_prefers_local_identity_over_correlated_id() {
8681 let installed = ListMatch {
8682 name: "ShareX".to_owned(),
8683 id: "ShareX.ShareX".to_owned(),
8684 local_id: r"ARP\Machine\X64\ShareX".to_owned(),
8685 installed_version: "19.0.2".to_owned(),
8686 available_version: None,
8687 source_name: Some("winget".to_owned()),
8688 publisher: Some("ShareX Team".to_owned()),
8689 scope: Some("Machine".to_owned()),
8690 installer_category: Some("exe".to_owned()),
8691 install_location: Some(r"C:\Program Files\ShareX".to_owned()),
8692 package_family_names: Vec::new(),
8693 product_codes: Vec::new(),
8694 upgrade_codes: Vec::new(),
8695 };
8696
8697 assert!(registry_entry_matches_installed_package(
8698 "ShareX", "ShareX", None, &installed
8699 ));
8700 assert!(!registry_entry_matches_installed_package(
8701 "ShareX.ShareX",
8702 "ShareX.ShareX",
8703 None,
8704 &installed
8705 ));
8706 }
8707
8708 #[test]
8709 #[cfg(windows)]
8710 fn build_uninstall_command_only_appends_silent_flag_when_needed() {
8711 assert_eq!(
8712 r#""C:\Program Files\ShareX\unins000.exe" /S"#,
8713 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe""#, true, false)
8714 );
8715 assert_eq!(
8716 r#""C:\Program Files\ShareX\unins000.exe" /VERYSILENT"#,
8717 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe" /VERYSILENT"#, true, false)
8718 );
8719 assert_eq!(
8720 r#""C:\Program Files\ShareX\unins000.exe""#,
8721 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe""#, false, false)
8722 );
8723 assert_eq!(
8724 "winget uninstall --product-code JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe",
8725 build_uninstall_command(
8726 "winget uninstall --product-code JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe",
8727 true,
8728 false,
8729 )
8730 );
8731 }
8732
8733 #[test]
8734 #[cfg(windows)]
8735 fn build_winget_uninstall_arguments_prefers_correlated_identity() {
8736 let installed = ListMatch {
8737 name: "lazygit".to_owned(),
8738 id: "JesseDuffield.lazygit".to_owned(),
8739 local_id: r"ARP\User\X64\JesseDuffield.lazygit".to_owned(),
8740 installed_version: "0.61.1".to_owned(),
8741 available_version: None,
8742 source_name: Some("winget".to_owned()),
8743 publisher: Some("Jesse Duffield".to_owned()),
8744 scope: Some("User".to_owned()),
8745 installer_category: Some("exe".to_owned()),
8746 install_location: Some(
8747 r"C:\Users\test\AppData\Local\Microsoft\WinGet\Packages\JesseDuffield.lazygit".to_owned(),
8748 ),
8749 package_family_names: Vec::new(),
8750 product_codes: vec!["JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe".to_owned()],
8751 upgrade_codes: Vec::new(),
8752 };
8753 let mut request = UninstallRequest::new(PackageQuery {
8754 id: Some("JesseDuffield.lazygit".to_owned()),
8755 install_scope: Some("user".to_owned()),
8756 ..PackageQuery::default()
8757 });
8758 request.mode = InstallerMode::Silent;
8759
8760 assert_eq!(
8761 Some(vec![
8762 "uninstall".to_owned(),
8763 "--id".to_owned(),
8764 "JesseDuffield.lazygit".to_owned(),
8765 "--exact".to_owned(),
8766 "--disable-interactivity".to_owned(),
8767 "--source".to_owned(),
8768 "winget".to_owned(),
8769 "--scope".to_owned(),
8770 "user".to_owned(),
8771 "--silent".to_owned(),
8772 ]),
8773 build_winget_uninstall_arguments(&installed, &request)
8774 );
8775
8776 assert_eq!(
8777 Some(vec![
8778 "uninstall".to_owned(),
8779 "--id".to_owned(),
8780 "JesseDuffield.lazygit".to_owned(),
8781 "--exact".to_owned(),
8782 "--disable-interactivity".to_owned(),
8783 "--source".to_owned(),
8784 "winget".to_owned(),
8785 "--silent".to_owned(),
8786 ]),
8787 build_winget_uninstall_arguments_with_scope(&installed, &request, false)
8788 );
8789 }
8790}