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