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 let installed_name = normalize_correlation_name(&package.name);
2652 let installed_name_lower = package.name.to_ascii_lowercase();
2653 let candidate_names = correlation_name_candidates(&package.name);
2654
2655 candidates
2656 .iter()
2657 .filter_map(|candidate| {
2658 let candidate_name = normalize_correlation_name(&candidate.name);
2659 let base_score = if candidate.id.eq_ignore_ascii_case(&package.local_id) {
2660 1000
2661 } else if candidate_names.iter().any(|name| {
2662 let normalized = normalize_correlation_name(name);
2663 normalized == candidate_name
2664 }) {
2665 900
2666 } else if allow_loose_name_match && candidate_name.len() >= 6 && installed_name.contains(&candidate_name) {
2667 700
2668 } else {
2669 0
2670 };
2671
2672 if base_score == 0 {
2673 return None;
2674 }
2675
2676 let candidate_lower = candidate.name.to_ascii_lowercase();
2689 let prefix_bonus = if installed_name_lower.starts_with(&candidate_lower) {
2690 100
2691 } else if installed_name_lower.contains(&candidate_lower) {
2692 50
2693 } else {
2694 0
2695 };
2696
2697 Some((base_score + prefix_bonus, candidate.clone()))
2698 })
2699 .max_by_key(|(score, _)| *score)
2700 .map(|(_, candidate)| candidate)
2701}
2702
2703fn correlation_name_candidates(name: &str) -> Vec<String> {
2704 let mut candidates = Vec::new();
2705 candidates.push(name.trim().to_owned());
2706
2707 let mut trimmed = name.trim().to_owned();
2708 if let Some(index) = trimmed.find(" (") {
2709 trimmed.truncate(index);
2710 }
2711
2712 let mut words = Vec::new();
2713 for token in trimmed.split_whitespace() {
2714 let lower = token
2715 .trim_matches(|ch: char| ch == '(' || ch == ')')
2716 .to_ascii_lowercase();
2717 if !words.is_empty()
2718 && (lower == "x64" || lower == "x86" || lower == "arm64" || token.chars().any(|ch| ch.is_ascii_digit()))
2719 {
2720 break;
2721 }
2722 words.push(token);
2723 }
2724
2725 if !words.is_empty() {
2726 candidates.push(words.join(" "));
2727 }
2728
2729 candidates.sort();
2730 candidates.dedup();
2731 candidates
2732}
2733
2734fn normalize_correlation_name(value: &str) -> String {
2735 value
2736 .chars()
2737 .filter(|ch| ch.is_ascii_alphanumeric())
2738 .flat_map(|ch| ch.to_lowercase())
2739 .collect()
2740}
2741
2742#[cfg(windows)]
2743fn collect_installed_packages(scope: Option<&str>) -> Result<Vec<InstalledPackage>> {
2744 let mut packages = Vec::new();
2745 let mut seen = BTreeSet::new();
2746
2747 let machine = !matches!(scope, Some(value) if value.eq_ignore_ascii_case("user"));
2748 let user = !matches!(scope, Some(value) if value.eq_ignore_ascii_case("machine"));
2749
2750 if machine {
2751 collect_uninstall_view(
2752 &mut packages,
2753 &mut seen,
2754 RegKey::predef(HKEY_LOCAL_MACHINE),
2755 "Machine",
2756 "X64",
2757 KEY_READ | KEY_WOW64_64KEY,
2758 )?;
2759 collect_uninstall_view(
2760 &mut packages,
2761 &mut seen,
2762 RegKey::predef(HKEY_LOCAL_MACHINE),
2763 "Machine",
2764 "X86",
2765 KEY_READ | KEY_WOW64_32KEY,
2766 )?;
2767 collect_appmodel_packages(
2768 &mut packages,
2769 &mut seen,
2770 RegKey::predef(HKEY_LOCAL_MACHINE),
2771 "Machine",
2772 KEY_READ | KEY_WOW64_64KEY,
2773 )?;
2774 }
2775
2776 if user {
2777 collect_uninstall_view(
2778 &mut packages,
2779 &mut seen,
2780 RegKey::predef(HKEY_CURRENT_USER),
2781 "User",
2782 "X64",
2783 KEY_READ,
2784 )?;
2785 collect_appmodel_packages(
2786 &mut packages,
2787 &mut seen,
2788 RegKey::predef(HKEY_CURRENT_USER),
2789 "User",
2790 KEY_READ,
2791 )?;
2792 }
2793
2794 Ok(packages)
2795}
2796
2797#[cfg(not(windows))]
2798fn collect_installed_packages(_scope: Option<&str>) -> Result<Vec<InstalledPackage>> {
2799 Ok(Vec::new())
2800}
2801
2802#[cfg(windows)]
2803fn collect_uninstall_view(
2804 packages: &mut Vec<InstalledPackage>,
2805 seen: &mut BTreeSet<String>,
2806 root: RegKey,
2807 scope: &str,
2808 arch: &str,
2809 flags: u32,
2810) -> Result<()> {
2811 const UNINSTALL_PATH: &str = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
2812
2813 let uninstall = match root.open_subkey_with_flags(UNINSTALL_PATH, flags) {
2814 Ok(key) => key,
2815 Err(_) => return Ok(()),
2816 };
2817
2818 for key_name in uninstall.enum_keys().flatten() {
2819 let subkey = match uninstall.open_subkey_with_flags(&key_name, flags) {
2820 Ok(key) => key,
2821 Err(_) => continue,
2822 };
2823 if read_reg_dword(&subkey, "SystemComponent") == Some(1) || read_reg_string(&subkey, "ParentKeyName").is_some()
2824 {
2825 continue;
2826 }
2827
2828 let Some(name) = read_reg_string(&subkey, "DisplayName").filter(|value| !value.is_empty()) else {
2829 continue;
2830 };
2831
2832 let local_id = format!(r"ARP\{scope}\{arch}\{key_name}");
2833 let installed_version = read_reg_string(&subkey, "DisplayVersion").unwrap_or_else(|| "Unknown".to_owned());
2834 let publisher = read_reg_string(&subkey, "Publisher");
2835 let install_location = read_reg_string(&subkey, "InstallLocation");
2836 let package_family_names = read_reg_string(&subkey, "PackageFamilyName")
2837 .into_iter()
2838 .collect::<Vec<_>>();
2839 let mut product_codes = read_reg_string(&subkey, "ProductCode").into_iter().collect::<Vec<_>>();
2840 if product_codes.is_empty() && looks_like_product_code(&key_name) {
2841 product_codes.push(key_name.to_ascii_lowercase());
2842 }
2843 let upgrade_codes = read_reg_string(&subkey, "UpgradeCode").into_iter().collect::<Vec<_>>();
2844 let installer_category =
2845 if local_id.starts_with("ARP\\") && read_reg_dword(&subkey, "WindowsInstaller") == Some(1) {
2846 Some("msi".to_owned())
2847 } else if key_name.starts_with("MSIX\\") {
2848 Some("msix".to_owned())
2849 } else {
2850 Some("exe".to_owned())
2851 };
2852
2853 let dedupe_key = format!(
2854 "{}|{}|{}|{}",
2855 local_id,
2856 name.to_ascii_lowercase(),
2857 installed_version.to_ascii_lowercase(),
2858 publisher.clone().unwrap_or_default().to_ascii_lowercase()
2859 );
2860 if !seen.insert(dedupe_key) {
2861 continue;
2862 }
2863
2864 packages.push(InstalledPackage {
2865 name,
2866 local_id,
2867 installed_version,
2868 publisher,
2869 scope: Some(scope.to_owned()),
2870 installer_category,
2871 install_location,
2872 package_family_names,
2873 product_codes,
2874 upgrade_codes,
2875 correlated: None,
2876 });
2877 }
2878
2879 Ok(())
2880}
2881
2882#[cfg(windows)]
2883fn collect_appmodel_packages(
2884 packages: &mut Vec<InstalledPackage>,
2885 seen: &mut BTreeSet<String>,
2886 root: RegKey,
2887 scope: &str,
2888 flags: u32,
2889) -> Result<()> {
2890 const APPMODEL_PACKAGES_PATH: &str =
2891 r"Software\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\Repository\Packages";
2892
2893 let appmodel = match root.open_subkey_with_flags(APPMODEL_PACKAGES_PATH, flags) {
2894 Ok(key) => key,
2895 Err(_) => return Ok(()),
2896 };
2897
2898 for key_name in appmodel.enum_keys().flatten() {
2899 let subkey = match appmodel.open_subkey_with_flags(&key_name, flags) {
2900 Ok(key) => key,
2901 Err(_) => continue,
2902 };
2903
2904 let Some(name) = read_reg_string(&subkey, "DisplayName").filter(|value| !value.is_empty()) else {
2905 continue;
2906 };
2907 let install_location = read_reg_string(&subkey, "PackageRootFolder");
2908 if install_location.as_deref().is_some_and(is_windows_system_path) {
2909 continue;
2910 }
2911
2912 let Some(metadata) = parse_msix_package_full_name(&key_name) else {
2913 continue;
2914 };
2915
2916 let local_id = format!(r"MSIX\{key_name}");
2917 let dedupe_key = format!(
2918 "{}|{}|{}",
2919 local_id,
2920 name.to_ascii_lowercase(),
2921 metadata.version.to_ascii_lowercase()
2922 );
2923 if !seen.insert(dedupe_key) {
2924 continue;
2925 }
2926
2927 packages.push(InstalledPackage {
2928 name,
2929 local_id,
2930 installed_version: metadata.version,
2931 publisher: None,
2932 scope: Some(scope.to_owned()),
2933 installer_category: Some("msix".to_owned()),
2934 install_location,
2935 package_family_names: vec![metadata.family_name],
2936 product_codes: Vec::new(),
2937 upgrade_codes: Vec::new(),
2938 correlated: None,
2939 });
2940 }
2941
2942 Ok(())
2943}
2944
2945#[cfg(windows)]
2946struct ParsedMsixPackageFullName {
2947 version: String,
2948 family_name: String,
2949}
2950
2951#[cfg(windows)]
2952fn parse_msix_package_full_name(value: &str) -> Option<ParsedMsixPackageFullName> {
2953 let segments = value.split('_').collect::<Vec<_>>();
2954 if segments.len() < 5 {
2955 return None;
2956 }
2957
2958 let name = segments[..segments.len() - 4].join("_");
2959 if name.is_empty() {
2960 return None;
2961 }
2962
2963 let version = segments[segments.len() - 4].trim();
2964 let resource_id = segments[segments.len() - 2].trim();
2965 let publisher_id = segments[segments.len() - 1].trim();
2966 if version.is_empty() || publisher_id.is_empty() {
2967 return None;
2968 }
2969
2970 let family_name = if resource_id.is_empty() {
2971 format!("{name}_{publisher_id}")
2972 } else {
2973 format!("{name}_{resource_id}_{publisher_id}")
2974 };
2975
2976 Some(ParsedMsixPackageFullName {
2977 version: version.to_owned(),
2978 family_name,
2979 })
2980}
2981
2982#[cfg(windows)]
2983fn is_windows_system_path(path: &str) -> bool {
2984 path.trim().to_ascii_lowercase().starts_with(r"c:\windows\")
2985}
2986
2987#[cfg(windows)]
2988fn read_reg_string(key: &RegKey, value_name: &str) -> Option<String> {
2989 key.get_value::<String, _>(value_name)
2990 .ok()
2991 .map(|value| value.trim().to_owned())
2992 .filter(|value| !value.is_empty())
2993}
2994
2995#[cfg(windows)]
2996fn read_reg_dword(key: &RegKey, value_name: &str) -> Option<u32> {
2997 key.get_value::<u32, _>(value_name).ok()
2998}
2999
3000#[cfg(windows)]
3001fn looks_like_product_code(value: &str) -> bool {
3002 value.starts_with('{') && value.ends_with('}')
3003}
3004
3005fn ensure_app_dirs(app_root: &Path) -> Result<()> {
3006 fs::create_dir_all(app_root).context("failed to create app root directory")?;
3007 if uses_packaged_layout(app_root) {
3008 fs::create_dir_all(packaged_file_cache_root(app_root)).context("failed to create packaged file cache root")?;
3009 let user_sources_path = packaged_user_sources_path(app_root)?;
3010 if let Some(parent) = user_sources_path.parent() {
3011 fs::create_dir_all(parent).context("failed to create packaged secure settings directory")?;
3012 }
3013 } else {
3014 fs::create_dir_all(app_root.join("sources")).context("failed to create app source directory")?;
3015 }
3016 Ok(())
3017}
3018
3019fn default_app_root() -> Result<PathBuf> {
3020 if let Some(app_root) = std::env::var_os("PINGET_APPROOT") {
3021 return Ok(PathBuf::from(app_root));
3022 }
3023
3024 let local_app_data = dirs::data_local_dir().ok_or_else(|| anyhow!("unable to determine LocalAppData path"))?;
3025
3026 #[cfg(windows)]
3027 {
3028 if has_current_package_identity() {
3029 return Ok(local_app_data
3030 .join("Packages")
3031 .join(PACKAGED_FAMILY_NAME)
3032 .join("LocalState"));
3033 }
3034 Ok(local_app_data.join("Devolutions").join("Pinget"))
3035 }
3036
3037 #[cfg(not(windows))]
3038 {
3039 Ok(local_app_data.join("pinget"))
3040 }
3041}
3042
3043#[cfg(windows)]
3044fn has_current_package_identity() -> bool {
3045 const APPMODEL_ERROR_NO_PACKAGE: u32 = 15700;
3048
3049 let mut length: u32 = 0;
3050 let rc = unsafe { GetCurrentPackageFullName(&mut length, std::ptr::null_mut()) };
3053 rc != APPMODEL_ERROR_NO_PACKAGE
3054}
3055
3056fn store_path(app_root: &Path) -> PathBuf {
3057 app_root.join("sources.json")
3058}
3059
3060fn user_settings_path(app_root: &Path) -> PathBuf {
3061 if uses_packaged_layout(app_root) {
3062 app_root.join("settings.json")
3063 } else {
3064 app_root.join("user-settings.json")
3065 }
3066}
3067
3068fn admin_settings_path(app_root: &Path) -> PathBuf {
3069 app_root.join("admin-settings.json")
3070}
3071
3072fn source_state_dir(app_root: &Path, source: &SourceRecord) -> PathBuf {
3073 if uses_packaged_layout(app_root) {
3074 return app_root
3075 .join(packaged_source_type(source.kind))
3076 .join(sanitize_path_segment(&source.identifier));
3077 }
3078
3079 app_root.join("sources").join(sanitize_path_segment(&source.name))
3080}
3081
3082fn preindexed_package_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3083 source_state_dir(app_root, source).join("source.msix")
3084}
3085
3086fn preindexed_index_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3087 source_state_dir(app_root, source).join("index.db")
3088}
3089
3090fn rest_information_cache_path(app_root: &Path, source: &SourceRecord) -> PathBuf {
3091 source_state_dir(app_root, source).join("rest-information.json")
3092}
3093
3094fn rest_manifest_cache_path_with_root(
3095 app_root: &Path,
3096 source: &SourceRecord,
3097 package_id: &str,
3098 version: &str,
3099 channel: &str,
3100) -> PathBuf {
3101 let key = format!("{}|{}|{}|{}", source.identifier, package_id, version, channel);
3102 let digest = sha256_hex(key.as_bytes());
3103 cache_root(app_root)
3104 .join("REST_M")
3105 .join(sanitize_path_segment(&source.identifier))
3106 .join(format!("{digest}.json"))
3107}
3108
3109fn temp_cache_path(app_root: &Path, bucket: &str, identifier: &str) -> PathBuf {
3110 cache_root(app_root)
3111 .join(bucket)
3112 .join(sanitize_path_segment(identifier))
3113}
3114
3115fn load_store(app_root: &Path) -> Result<SourceStore> {
3116 if uses_packaged_layout(app_root)
3117 && let Some(store) = load_packaged_store(app_root)?
3118 {
3119 return Ok(store);
3120 }
3121
3122 let path = store_path(app_root);
3123 if !path.exists() {
3124 let store = SourceStore::default();
3125 save_store(app_root, &store)?;
3126 return Ok(store);
3127 }
3128
3129 let bytes = fs::read(path).context("failed to read source store")?;
3130 serde_json::from_slice(&bytes).context("failed to parse source store")
3131}
3132
3133fn save_store(app_root: &Path, store: &SourceStore) -> Result<()> {
3134 if uses_system_winget_source_commands(app_root) {
3135 bail!("System WinGet source settings must be modified through winget.");
3136 }
3137
3138 if uses_packaged_layout(app_root) {
3139 return save_packaged_store(app_root, store);
3140 }
3141
3142 write_json(store_path(app_root), store)
3143}
3144
3145fn load_packaged_store(app_root: &Path) -> Result<Option<SourceStore>> {
3146 let user_sources_path = packaged_user_sources_path(app_root)?;
3147 let metadata_path = app_root.join("sources_metadata");
3148 let user_sources_yaml = fs::read_to_string(&user_sources_path).ok();
3149 let metadata_yaml = fs::read_to_string(&metadata_path).ok();
3150 load_packaged_store_from_streams(user_sources_yaml.as_deref(), metadata_yaml.as_deref())
3151}
3152
3153fn load_packaged_store_from_streams(
3154 user_sources_yaml: Option<&str>,
3155 metadata_yaml: Option<&str>,
3156) -> Result<Option<SourceStore>> {
3157 if is_secure_settings_stub(user_sources_yaml) {
3158 return load_system_winget_source_store().map(Some);
3159 }
3160
3161 Ok(parse_packaged_source_store(user_sources_yaml, metadata_yaml))
3162}
3163
3164fn save_packaged_store(app_root: &Path, store: &SourceStore) -> Result<()> {
3165 let user_sources_path = packaged_user_sources_path(app_root)?;
3166 if let Some(parent) = user_sources_path.parent() {
3167 fs::create_dir_all(parent).context("failed to create packaged source stream directory")?;
3168 }
3169
3170 fs::write(&user_sources_path, render_packaged_sources_yaml(store))
3171 .context("failed to write packaged user_sources stream")?;
3172 fs::write(app_root.join("sources_metadata"), render_packaged_metadata_yaml(store))
3173 .context("failed to write packaged source metadata")?;
3174 Ok(())
3175}
3176
3177fn uses_system_winget_source_commands(app_root: &Path) -> bool {
3178 if !uses_packaged_layout(app_root) {
3179 return false;
3180 }
3181
3182 #[cfg(windows)]
3183 {
3184 let Ok(user_sources_path) = packaged_user_sources_path(app_root) else {
3185 return false;
3186 };
3187
3188 let user_sources_yaml = fs::read_to_string(user_sources_path).ok();
3189 is_secure_settings_stub(user_sources_yaml.as_deref())
3190 }
3191
3192 #[cfg(not(windows))]
3193 {
3194 false
3195 }
3196}
3197
3198fn is_secure_settings_stub(value: Option<&str>) -> bool {
3199 value
3200 .map(str::trim_start)
3201 .is_some_and(|value| value.starts_with("SHA256:") || value.starts_with("sha256:"))
3202}
3203
3204fn load_system_winget_source_store() -> Result<SourceStore> {
3205 let result = run_checked_winget_source_command(build_winget_source_export_arguments(), "export WinGet sources")?;
3206 Ok(SourceStore {
3207 sources: parse_system_winget_source_export(&result.stdout)?,
3208 })
3209}
3210
3211fn system_winget_add_source(name: &str, arg: &str, kind: SourceKind, trust_level: &str, explicit: bool) -> Result<()> {
3212 run_checked_winget_source_command(
3213 build_winget_source_add_arguments(name, arg, kind, trust_level, explicit),
3214 &format!("add WinGet source '{name}'"),
3215 )?;
3216 Ok(())
3217}
3218
3219fn system_winget_remove_source(name: &str) -> Result<()> {
3220 run_checked_winget_source_command(
3221 vec![
3222 "source".to_owned(),
3223 "remove".to_owned(),
3224 "--name".to_owned(),
3225 name.to_owned(),
3226 "--disable-interactivity".to_owned(),
3227 ],
3228 &format!("remove WinGet source '{name}'"),
3229 )?;
3230 Ok(())
3231}
3232
3233fn system_winget_reset_source(name: &str) -> Result<()> {
3234 run_checked_winget_source_command(
3235 vec![
3236 "source".to_owned(),
3237 "reset".to_owned(),
3238 "--name".to_owned(),
3239 name.to_owned(),
3240 "--force".to_owned(),
3241 "--disable-interactivity".to_owned(),
3242 ],
3243 &format!("reset WinGet source '{name}'"),
3244 )?;
3245 Ok(())
3246}
3247
3248fn system_winget_reset_sources() -> Result<()> {
3249 run_checked_winget_source_command(
3250 vec![
3251 "source".to_owned(),
3252 "reset".to_owned(),
3253 "--force".to_owned(),
3254 "--disable-interactivity".to_owned(),
3255 ],
3256 "reset WinGet sources",
3257 )?;
3258 Ok(())
3259}
3260
3261fn system_winget_update_sources(name: Option<&str>) -> Result<String> {
3262 let mut args = vec!["source".to_owned(), "update".to_owned()];
3263 if let Some(name) = name {
3264 args.push("--name".to_owned());
3265 args.push(name.to_owned());
3266 }
3267
3268 args.push("--disable-interactivity".to_owned());
3269 let result = run_checked_winget_source_command(
3270 args,
3271 if name.is_some() {
3272 "update WinGet source"
3273 } else {
3274 "update WinGet sources"
3275 },
3276 )?;
3277 let detail = result.stdout.trim();
3278 Ok(if detail.is_empty() {
3279 "Done".to_owned()
3280 } else {
3281 detail.to_owned()
3282 })
3283}
3284
3285fn build_winget_source_export_arguments() -> Vec<String> {
3286 vec![
3287 "source".to_owned(),
3288 "export".to_owned(),
3289 "--disable-interactivity".to_owned(),
3290 ]
3291}
3292
3293fn build_winget_source_add_arguments(
3294 name: &str,
3295 arg: &str,
3296 kind: SourceKind,
3297 trust_level: &str,
3298 explicit: bool,
3299) -> Vec<String> {
3300 let mut args = vec![
3301 "source".to_owned(),
3302 "add".to_owned(),
3303 "--name".to_owned(),
3304 name.to_owned(),
3305 "--arg".to_owned(),
3306 arg.to_owned(),
3307 "--type".to_owned(),
3308 packaged_source_type(kind).to_owned(),
3309 "--disable-interactivity".to_owned(),
3310 ];
3311
3312 if !trust_level.trim().is_empty() {
3313 args.push("--trust-level".to_owned());
3314 args.push(if trust_level.eq_ignore_ascii_case("Trusted") {
3315 "trusted".to_owned()
3316 } else {
3317 "none".to_owned()
3318 });
3319 }
3320
3321 if explicit {
3322 args.push("--explicit".to_owned());
3323 }
3324
3325 args
3326}
3327
3328#[derive(Debug)]
3329struct WingetSourceCommandResult {
3330 exit_code: i32,
3331 stdout: String,
3332 stderr: String,
3333}
3334
3335type WingetSourceCommandRunner = fn(&[String]) -> Result<WingetSourceCommandResult>;
3336
3337static SYSTEM_WINGET_SOURCE_COMMAND_RUNNER: RwLock<WingetSourceCommandRunner> =
3338 RwLock::new(run_winget_source_command_process);
3339
3340fn run_checked_winget_source_command(args: Vec<String>, action: &str) -> Result<WingetSourceCommandResult> {
3341 let result = run_winget_source_command(&args)?;
3342 if result.exit_code == 0 {
3343 return Ok(result);
3344 }
3345
3346 let detail = [result.stderr.trim(), result.stdout.trim()]
3347 .into_iter()
3348 .filter(|value| !value.is_empty())
3349 .collect::<Vec<_>>()
3350 .join("\n");
3351 if detail.is_empty() {
3352 bail!("Failed to {action}; winget exited with code {}.", result.exit_code);
3353 }
3354
3355 bail!(
3356 "Failed to {action}; winget exited with code {}:\n{}",
3357 result.exit_code,
3358 detail
3359 );
3360}
3361
3362fn run_winget_source_command(args: &[String]) -> Result<WingetSourceCommandResult> {
3363 let runner = *SYSTEM_WINGET_SOURCE_COMMAND_RUNNER
3364 .read()
3365 .map_err(|_| anyhow!("failed to read WinGet source command runner"))?;
3366 runner(args)
3367}
3368
3369fn run_winget_source_command_process(args: &[String]) -> Result<WingetSourceCommandResult> {
3370 use std::process::Command;
3371
3372 let output = Command::new("winget")
3373 .args(args)
3374 .output()
3375 .context("failed to run winget source command")?;
3376
3377 Ok(WingetSourceCommandResult {
3378 exit_code: output.status.code().unwrap_or(-1),
3379 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
3380 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
3381 })
3382}
3383
3384fn parse_system_winget_source_export(output: &str) -> Result<Vec<SourceRecord>> {
3385 let trimmed = output.trim();
3386 if trimmed.is_empty() {
3387 return Ok(Vec::new());
3388 }
3389
3390 if let Ok(value) = serde_json::from_str::<JsonValue>(trimmed)
3391 && let Some(sources) = parse_system_winget_source_export_value(&value)?
3392 {
3393 return Ok(order_sources(sources));
3394 }
3395
3396 let mut sources = Vec::new();
3397 for line in output.replace("\r\n", "\n").lines().map(str::trim) {
3398 if line.is_empty() {
3399 continue;
3400 }
3401
3402 let value: JsonValue = serde_json::from_str(line)
3403 .with_context(|| format!("WinGet source export returned a non-source line: {line}"))?;
3404 let Some(mut parsed) = parse_system_winget_source_export_value(&value)? else {
3405 bail!("WinGet source export returned a non-source line: {line}");
3406 };
3407 sources.append(&mut parsed);
3408 }
3409
3410 Ok(order_sources(sources))
3411}
3412
3413fn parse_system_winget_source_export_value(value: &JsonValue) -> Result<Option<Vec<SourceRecord>>> {
3414 match value {
3415 JsonValue::Array(items) => items
3416 .iter()
3417 .map(parse_system_winget_source_record)
3418 .collect::<Result<Vec<_>>>()
3419 .map(Some),
3420 JsonValue::Object(object) => {
3421 if let Some(JsonValue::Array(items)) = object.get("Sources") {
3422 items
3423 .iter()
3424 .map(parse_system_winget_source_record)
3425 .collect::<Result<Vec<_>>>()
3426 .map(Some)
3427 } else if object.contains_key("Name") {
3428 Ok(Some(vec![parse_system_winget_source_record(value)?]))
3429 } else {
3430 Ok(None)
3431 }
3432 }
3433 _ => Ok(None),
3434 }
3435}
3436
3437fn parse_system_winget_source_record(value: &JsonValue) -> Result<SourceRecord> {
3438 let object = value
3439 .as_object()
3440 .ok_or_else(|| anyhow!("WinGet source export entry was not an object"))?;
3441 let name = required_json_string(object, "Name")?;
3442 let source_type = required_json_string(object, "Type")?;
3443 let arg = required_json_string(object, "Arg")?;
3444 let kind = parse_system_winget_source_kind(&source_type)?;
3445 let identifier = optional_json_string(object, "Identifier")
3446 .or_else(|| optional_json_string(object, "Data"))
3447 .unwrap_or_else(|| name.clone());
3448
3449 Ok(SourceRecord {
3450 name,
3451 kind,
3452 arg,
3453 identifier,
3454 trust_level: parse_system_winget_trust_level(object),
3455 explicit: optional_json_bool(object, "Explicit").unwrap_or(false),
3456 priority: optional_json_i32(object, "Priority").unwrap_or_default(),
3457 last_update: None,
3458 source_version: None,
3459 })
3460}
3461
3462fn required_json_string(object: &JsonMap<String, JsonValue>, key: &str) -> Result<String> {
3463 optional_json_string(object, key).ok_or_else(|| anyhow!("WinGet source export did not include '{key}'."))
3464}
3465
3466fn optional_json_string(object: &JsonMap<String, JsonValue>, key: &str) -> Option<String> {
3467 match object.get(key)? {
3468 JsonValue::String(value) => Some(value.clone()),
3469 JsonValue::Number(value) => Some(value.to_string()),
3470 _ => None,
3471 }
3472}
3473
3474fn optional_json_bool(object: &JsonMap<String, JsonValue>, key: &str) -> Option<bool> {
3475 match object.get(key)? {
3476 JsonValue::Bool(value) => Some(*value),
3477 JsonValue::String(value) => value.parse().ok(),
3478 _ => None,
3479 }
3480}
3481
3482fn optional_json_i32(object: &JsonMap<String, JsonValue>, key: &str) -> Option<i32> {
3483 match object.get(key)? {
3484 JsonValue::Number(value) => value.as_i64().and_then(|value| i32::try_from(value).ok()),
3485 JsonValue::String(value) => value.parse().ok(),
3486 _ => None,
3487 }
3488}
3489
3490fn parse_system_winget_source_kind(value: &str) -> Result<SourceKind> {
3491 if value.eq_ignore_ascii_case("Microsoft.PreIndexed.Package") {
3492 Ok(SourceKind::PreIndexed)
3493 } else if value.eq_ignore_ascii_case("Microsoft.Rest") {
3494 Ok(SourceKind::Rest)
3495 } else {
3496 bail!("Unsupported WinGet source type '{value}'.")
3497 }
3498}
3499
3500fn parse_system_winget_trust_level(object: &JsonMap<String, JsonValue>) -> String {
3501 let Some(value) = object.get("TrustLevel") else {
3502 return "None".to_owned();
3503 };
3504
3505 match value {
3506 JsonValue::Array(items) if items.iter().any(json_trust_value_is_trusted) => "Trusted".to_owned(),
3507 JsonValue::String(_) if json_trust_value_is_trusted(value) => "Trusted".to_owned(),
3508 JsonValue::Number(value) if value.as_i64().unwrap_or_default() > 0 => "Trusted".to_owned(),
3509 _ => "None".to_owned(),
3510 }
3511}
3512
3513fn json_trust_value_is_trusted(value: &JsonValue) -> bool {
3514 value
3515 .as_str()
3516 .is_some_and(|value| value.eq_ignore_ascii_case("Trusted"))
3517}
3518
3519fn order_sources(mut sources: Vec<SourceRecord>) -> Vec<SourceRecord> {
3520 sources.sort_by(|left, right| {
3521 left.explicit
3522 .cmp(&right.explicit)
3523 .then_with(|| right.priority.cmp(&left.priority))
3524 .then_with(|| left.name.to_ascii_lowercase().cmp(&right.name.to_ascii_lowercase()))
3525 });
3526 sources
3527}
3528
3529fn parse_packaged_source_store(user_sources_yaml: Option<&str>, metadata_yaml: Option<&str>) -> Option<SourceStore> {
3530 if user_sources_yaml.is_none() && metadata_yaml.is_none() {
3531 return None;
3532 }
3533
3534 let mut sources = SourceStore::default().sources;
3535 for entry in parse_packaged_yaml_entries(user_sources_yaml) {
3536 let Some(name) = entry.get("Name").and_then(|value| value.clone()) else {
3537 continue;
3538 };
3539
3540 if yaml_bool(entry.get("IsTombstone")) {
3541 sources.retain(|source| !source.name.eq_ignore_ascii_case(&name));
3542 continue;
3543 }
3544
3545 let Some(mapped) = map_packaged_source_entry(&entry) else {
3546 continue;
3547 };
3548
3549 if let Some(existing) = sources
3550 .iter_mut()
3551 .find(|source| source.name.eq_ignore_ascii_case(&name))
3552 {
3553 *existing = mapped;
3554 } else {
3555 sources.push(mapped);
3556 }
3557 }
3558
3559 for entry in parse_packaged_yaml_entries(metadata_yaml) {
3560 let Some(name) = entry.get("Name").and_then(|value| value.clone()) else {
3561 continue;
3562 };
3563
3564 let Some(source) = sources
3565 .iter_mut()
3566 .find(|source| source.name.eq_ignore_ascii_case(&name))
3567 else {
3568 continue;
3569 };
3570
3571 if let Some(last_update) = entry
3572 .get("LastUpdate")
3573 .and_then(|value| value.as_deref())
3574 .and_then(parse_packaged_datetime)
3575 {
3576 source.last_update = Some(last_update);
3577 }
3578
3579 if let Some(source_version) = entry.get("SourceVersion").and_then(|value| value.clone())
3580 && !source_version.is_empty()
3581 {
3582 source.source_version = Some(source_version);
3583 }
3584 }
3585
3586 Some(SourceStore { sources })
3587}
3588
3589fn parse_packaged_yaml_entries(yaml: Option<&str>) -> Vec<BTreeMap<String, Option<String>>> {
3590 let Some(yaml) = yaml else {
3591 return Vec::new();
3592 };
3593
3594 let mut entries = Vec::new();
3595 let mut current: Option<BTreeMap<String, Option<String>>> = None;
3596 for raw_line in yaml.replace("\r\n", "\n").split('\n') {
3597 let line = raw_line.trim_end();
3598 let trimmed = line.trim();
3599 if trimmed.is_empty() || trimmed.starts_with('#') || trimmed.eq_ignore_ascii_case("Sources:") {
3600 continue;
3601 }
3602
3603 if let Some(remainder) = trimmed.strip_prefix("- ") {
3604 if let Some(entry) = current.take() {
3605 entries.push(entry);
3606 }
3607
3608 let mut entry = BTreeMap::new();
3609 add_packaged_yaml_key_value(&mut entry, remainder);
3610 current = Some(entry);
3611 continue;
3612 }
3613
3614 if let Some(entry) = current.as_mut() {
3615 add_packaged_yaml_key_value(entry, trimmed);
3616 }
3617 }
3618
3619 if let Some(entry) = current {
3620 entries.push(entry);
3621 }
3622
3623 entries
3624}
3625
3626fn add_packaged_yaml_key_value(entry: &mut BTreeMap<String, Option<String>>, line: &str) {
3627 let Some((key, value)) = line.split_once(':') else {
3628 return;
3629 };
3630
3631 entry.insert(
3632 key.trim().to_owned(),
3633 Some(unquote_yaml_scalar(value.trim()).to_owned()),
3634 );
3635}
3636
3637fn unquote_yaml_scalar(value: &str) -> &str {
3638 if value.len() >= 2 {
3639 let bytes = value.as_bytes();
3640 let first = bytes[0] as char;
3641 let last = bytes[value.len() - 1] as char;
3642 if (first == '"' && last == '"') || (first == '\'' && last == '\'') {
3643 return &value[1..value.len() - 1];
3644 }
3645 }
3646
3647 value
3648}
3649
3650fn map_packaged_source_entry(entry: &BTreeMap<String, Option<String>>) -> Option<SourceRecord> {
3651 let name = entry.get("Name").and_then(|value| value.clone())?;
3652 let source_type = entry.get("Type").and_then(|value| value.clone())?;
3653 let arg = entry.get("Arg").and_then(|value| value.clone())?;
3654 let kind = parse_packaged_source_kind(&source_type)?;
3655
3656 Some(SourceRecord {
3657 name: name.clone(),
3658 kind,
3659 arg,
3660 identifier: entry
3661 .get("Identifier")
3662 .and_then(|value| value.clone())
3663 .or_else(|| entry.get("Data").and_then(|value| value.clone()))
3664 .unwrap_or(name),
3665 trust_level: parse_packaged_trust_level(entry.get("TrustLevel").and_then(|value| value.as_deref())),
3666 explicit: yaml_bool(entry.get("Explicit")),
3667 priority: entry
3668 .get("Priority")
3669 .and_then(|value| value.as_deref())
3670 .and_then(|value| value.parse::<i32>().ok())
3671 .unwrap_or_default(),
3672 last_update: None,
3673 source_version: None,
3674 })
3675}
3676
3677fn parse_packaged_source_kind(value: &str) -> Option<SourceKind> {
3678 if value.eq_ignore_ascii_case("Microsoft.PreIndexed.Package") {
3679 Some(SourceKind::PreIndexed)
3680 } else if value.eq_ignore_ascii_case("Microsoft.Rest") {
3681 Some(SourceKind::Rest)
3682 } else {
3683 None
3684 }
3685}
3686
3687fn parse_packaged_trust_level(value: Option<&str>) -> String {
3688 match value {
3689 Some(raw) if raw.eq_ignore_ascii_case("Trusted") => "Trusted".to_owned(),
3690 Some(raw) if raw.parse::<i64>().ok().unwrap_or_default() > 0 => "Trusted".to_owned(),
3691 _ => "None".to_owned(),
3692 }
3693}
3694
3695fn yaml_bool(value: Option<&Option<String>>) -> bool {
3696 value
3697 .and_then(|raw| raw.as_deref())
3698 .and_then(|raw| raw.parse::<bool>().ok())
3699 .unwrap_or(false)
3700}
3701
3702fn parse_packaged_datetime(value: &str) -> Option<DateTime<Utc>> {
3703 if let Ok(unix_seconds) = value.parse::<i64>() {
3704 return DateTime::<Utc>::from_timestamp(unix_seconds, 0);
3705 }
3706
3707 DateTime::parse_from_rfc3339(value)
3708 .ok()
3709 .map(|value| value.with_timezone(&Utc))
3710}
3711
3712fn render_packaged_sources_yaml(store: &SourceStore) -> String {
3713 let mut lines = vec!["Sources:".to_owned()];
3714 for source in &store.sources {
3715 lines.push(format!(" - Name: {}", source.name));
3716 lines.push(format!(" Type: {}", packaged_source_type(source.kind)));
3717 lines.push(format!(" Arg: {}", yaml_scalar(&source.arg)));
3718 lines.push(format!(" Data: {}", yaml_scalar(&source.identifier)));
3719 lines.push(format!(
3720 " TrustLevel: {}",
3721 if source.trust_level.eq_ignore_ascii_case("Trusted") {
3722 1
3723 } else {
3724 0
3725 }
3726 ));
3727 lines.push(format!(" Explicit: {}", source.explicit));
3728 lines.push(format!(" Priority: {}", source.priority));
3729 lines.push(" IsTombstone: false".to_owned());
3730 }
3731 lines.push(String::new());
3732 lines.join("\n")
3733}
3734
3735fn render_packaged_metadata_yaml(store: &SourceStore) -> String {
3736 let mut lines = vec!["Sources:".to_owned()];
3737 for source in &store.sources {
3738 if source.last_update.is_none() && source.source_version.is_none() {
3739 continue;
3740 }
3741
3742 lines.push(format!(" - Name: {}", source.name));
3743 if let Some(last_update) = source.last_update {
3744 lines.push(format!(" LastUpdate: {}", last_update.timestamp()));
3745 }
3746 if let Some(source_version) = &source.source_version {
3747 lines.push(format!(" SourceVersion: {}", yaml_scalar(source_version)));
3748 }
3749 }
3750 lines.push(String::new());
3751 lines.join("\n")
3752}
3753
3754fn yaml_scalar(value: &str) -> String {
3755 if value.chars().any(|ch| ch.is_whitespace() || ch == ':') {
3756 format!("\"{}\"", value.replace('"', "\\\""))
3757 } else {
3758 value.to_owned()
3759 }
3760}
3761
3762fn uses_packaged_layout(app_root: &Path) -> bool {
3763 #[cfg(windows)]
3764 {
3765 try_get_packaged_local_state_root(app_root).is_some()
3766 }
3767
3768 #[cfg(not(windows))]
3769 {
3770 let _ = app_root;
3771 false
3772 }
3773}
3774
3775#[cfg(windows)]
3776fn try_get_packaged_local_state_root(app_root: &Path) -> Option<PathBuf> {
3777 let local_state = app_root.file_name()?.to_string_lossy();
3778 if !local_state.eq_ignore_ascii_case("LocalState") {
3779 return None;
3780 }
3781
3782 let family = app_root.parent()?.file_name()?.to_string_lossy();
3783 if !family.eq_ignore_ascii_case(PACKAGED_FAMILY_NAME) {
3784 return None;
3785 }
3786
3787 let packages = app_root.parent()?.parent()?.file_name()?.to_string_lossy();
3788 if !packages.eq_ignore_ascii_case("Packages") {
3789 return None;
3790 }
3791
3792 Some(app_root.to_path_buf())
3793}
3794
3795#[cfg(windows)]
3796fn packaged_user_sources_path(app_root: &Path) -> Result<PathBuf> {
3797 let _ = try_get_packaged_local_state_root(app_root).ok_or_else(|| anyhow!("not a packaged WinGet app root"))?;
3798 Ok(packaged_secure_settings_root()?.join("user_sources"))
3799}
3800
3801#[cfg(not(windows))]
3802fn packaged_user_sources_path(_app_root: &Path) -> Result<PathBuf> {
3803 bail!("packaged WinGet source streams are only available on Windows")
3804}
3805
3806#[cfg(windows)]
3807fn packaged_secure_settings_root() -> Result<PathBuf> {
3808 let program_data = std::env::var_os("PROGRAMDATA")
3809 .map(PathBuf::from)
3810 .ok_or_else(|| anyhow!("unable to determine ProgramData path"))?;
3811 Ok(program_data
3812 .join("Microsoft")
3813 .join("WinGet")
3814 .join(current_user_sid()?)
3815 .join("settings")
3816 .join("pkg")
3817 .join(PACKAGED_NAME))
3818}
3819
3820#[cfg(windows)]
3821#[allow(clippy::multiple_unsafe_ops_per_block)]
3822fn current_user_sid() -> Result<String> {
3823 fn close_token(token: HANDLE) {
3824 if !token.is_null() {
3825 unsafe {
3827 CloseHandle(token);
3828 }
3829 }
3830 }
3831
3832 let mut token: HANDLE = std::ptr::null_mut();
3833 let current_process = unsafe { GetCurrentProcess() };
3835 if unsafe { OpenProcessToken(current_process, TOKEN_QUERY, &mut token) } == 0 {
3837 bail!("failed to open current process token")
3838 }
3839
3840 let mut length = 0;
3841 unsafe {
3843 GetTokenInformation(token, TokenUser, std::ptr::null_mut(), 0, &mut length);
3844 }
3845
3846 let element_size = size_of::<TOKEN_USER>();
3847 let element_count = (length as usize).div_ceil(element_size);
3848 let mut buffer = vec![MaybeUninit::<TOKEN_USER>::uninit(); element_count];
3849 if unsafe { GetTokenInformation(token, TokenUser, buffer.as_mut_ptr().cast(), length, &mut length) } == 0 {
3851 close_token(token);
3852 bail!("failed to get current user token information")
3853 }
3854
3855 let token_user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
3857 let mut sid_ptr: *mut u16 = std::ptr::null_mut();
3858 if unsafe { ConvertSidToStringSidW(token_user.User.Sid, &mut sid_ptr) } == 0 {
3860 close_token(token);
3861 bail!("failed to convert current user SID")
3862 }
3863
3864 let mut len = 0usize;
3865 while unsafe { *sid_ptr.wrapping_add(len) } != 0 {
3867 len += 1;
3868 }
3869
3870 let sid_slice = unsafe { std::slice::from_raw_parts(sid_ptr, len) };
3872 let sid = std::ffi::OsString::from_wide(sid_slice).to_string_lossy().into_owned();
3873 unsafe {
3875 LocalFree(sid_ptr.cast());
3876 }
3877 close_token(token);
3878 Ok(sid)
3879}
3880
3881fn packaged_file_cache_root(app_root: &Path) -> PathBuf {
3882 app_root.join("Microsoft").join("Windows Package Manager")
3883}
3884
3885fn cache_root(app_root: &Path) -> PathBuf {
3886 if uses_packaged_layout(app_root) {
3887 packaged_file_cache_root(app_root)
3888 } else {
3889 default_cache_root_fallback()
3890 }
3891}
3892
3893fn default_cache_root_fallback() -> PathBuf {
3894 std::env::temp_dir().join("cache")
3895}
3896
3897fn packaged_source_type(kind: SourceKind) -> &'static str {
3898 match kind {
3899 SourceKind::PreIndexed => "Microsoft.PreIndexed.Package",
3900 SourceKind::Rest => "Microsoft.Rest",
3901 }
3902}
3903
3904fn sanitize_path_segment(value: &str) -> String {
3905 value
3906 .chars()
3907 .map(|ch| match ch {
3908 '\\' | '/' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
3909 _ => ch,
3910 })
3911 .collect()
3912}
3913
3914fn load_json_object(path: &Path) -> Result<JsonMap<String, JsonValue>> {
3915 if !path.exists() {
3916 return Ok(JsonMap::new());
3917 }
3918
3919 let bytes = fs::read(path).context("failed to read JSON file")?;
3920 let value: JsonValue = serde_json::from_slice(&bytes).context("failed to parse JSON file")?;
3921 Ok(match value {
3922 JsonValue::Object(object) => object,
3923 _ => JsonMap::new(),
3924 })
3925}
3926
3927fn save_json_object(path: PathBuf, value: &JsonMap<String, JsonValue>) -> Result<()> {
3928 write_json(path, value)
3929}
3930
3931fn write_json<T: Serialize>(path: PathBuf, value: &T) -> Result<()> {
3932 if let Some(parent) = path.parent() {
3933 fs::create_dir_all(parent).context("failed to create JSON parent directory")?;
3934 }
3935
3936 let bytes = serde_json::to_vec_pretty(value).context("failed to serialize JSON")?;
3937 fs::write(path, bytes).context("failed to write JSON file")
3938}
3939
3940fn merge_json_objects(
3941 current: &JsonMap<String, JsonValue>,
3942 update: &JsonMap<String, JsonValue>,
3943) -> JsonMap<String, JsonValue> {
3944 let mut merged = current.clone();
3945 for (key, value) in update {
3946 match (merged.get(key), value) {
3947 (Some(JsonValue::Object(current_object)), JsonValue::Object(update_object)) => {
3948 merged.insert(
3949 key.clone(),
3950 JsonValue::Object(merge_json_objects(current_object, update_object)),
3951 );
3952 }
3953 _ => {
3954 merged.insert(key.clone(), value.clone());
3955 }
3956 }
3957 }
3958
3959 merged
3960}
3961
3962fn json_contains(current: &JsonValue, expected: &JsonValue) -> bool {
3963 match expected {
3964 JsonValue::Object(expected_object) => match current {
3965 JsonValue::Object(current_object) => expected_object.iter().all(|(key, expected_child)| {
3966 current_object
3967 .get(key)
3968 .is_some_and(|current_child| json_contains(current_child, expected_child))
3969 }),
3970 _ => false,
3971 },
3972 JsonValue::Array(expected_array) => match current {
3973 JsonValue::Array(current_array) => {
3974 current_array.len() == expected_array.len()
3975 && current_array
3976 .iter()
3977 .zip(expected_array.iter())
3978 .all(|(current_child, expected_child)| json_contains(current_child, expected_child))
3979 }
3980 _ => false,
3981 },
3982 _ => current == expected,
3983 }
3984}
3985
3986fn normalize_source_trust_level(trust_level: Option<&str>) -> Result<String> {
3987 match trust_level.unwrap_or("none").trim().to_ascii_lowercase().as_str() {
3988 "none" | "default" => Ok("None".to_owned()),
3989 "trusted" => Ok("Trusted".to_owned()),
3990 other => bail!("unsupported source trust level: {other}"),
3991 }
3992}
3993
3994fn normalize_admin_setting_name(name: &str) -> Result<&'static str> {
3995 SUPPORTED_ADMIN_SETTINGS
3996 .iter()
3997 .copied()
3998 .find(|setting| setting.eq_ignore_ascii_case(name))
3999 .ok_or_else(|| anyhow!("unsupported admin setting: {name}"))
4000}
4001
4002fn default_source_trust_level() -> String {
4003 "None".to_owned()
4004}
4005
4006fn query_rows<T, F>(connection: &Connection, sql: &str, params: Vec<SqlValue>, mut map: F) -> Result<Vec<T>>
4007where
4008 F: FnMut(&SqlRow<'_>) -> Result<T>,
4009{
4010 let mut statement = connection
4011 .prepare(sql)
4012 .with_context(|| format!("failed to prepare SQL query: {sql}"))?;
4013 let mut rows = statement
4014 .query(params_from_iter(params))
4015 .with_context(|| format!("failed to execute SQL query: {sql}"))?;
4016 let mut result = Vec::new();
4017
4018 while let Some(row) = rows.next().context("failed to read SQL row")? {
4019 result.push(map(row)?);
4020 }
4021
4022 Ok(result)
4023}
4024
4025fn query_optional_value<T, F>(connection: &Connection, sql: &str, params: Vec<SqlValue>, map: F) -> Result<Option<T>>
4026where
4027 F: FnOnce(&SqlRow<'_>) -> Result<T>,
4028{
4029 let mut statement = connection
4030 .prepare(sql)
4031 .with_context(|| format!("failed to prepare SQL query: {sql}"))?;
4032 let mut rows = statement
4033 .query(params_from_iter(params))
4034 .with_context(|| format!("failed to execute SQL query: {sql}"))?;
4035
4036 match rows.next().context("failed to read SQL row")? {
4037 Some(row) => Ok(Some(map(row)?)),
4038 None => Ok(None),
4039 }
4040}
4041
4042fn row_ref<'a>(row: &'a SqlRow<'_>, index: usize) -> Result<ValueRef<'a>> {
4043 row.get_ref(index)
4044 .with_context(|| format!("failed to read SQL column {index}"))
4045}
4046
4047fn row_string(row: &SqlRow<'_>, index: usize) -> Result<String> {
4048 match row_ref(row, index)? {
4049 ValueRef::Text(value) => std::str::from_utf8(value)
4050 .context("SQL text column was not valid UTF-8")
4051 .map(str::to_owned),
4052 ValueRef::Integer(value) => Ok(value.to_string()),
4053 ValueRef::Real(value) => Ok(value.to_string()),
4054 ValueRef::Blob(value) => String::from_utf8(value.to_vec()).context("SQL blob column was not valid UTF-8"),
4055 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4056 }
4057}
4058
4059fn row_opt_string(row: &SqlRow<'_>, index: usize) -> Result<Option<String>> {
4060 match row_ref(row, index)? {
4061 ValueRef::Null => Ok(None),
4062 ValueRef::Text(value) => Ok(Some(
4063 std::str::from_utf8(value)
4064 .context("SQL text column was not valid UTF-8")?
4065 .to_owned(),
4066 )),
4067 ValueRef::Integer(value) => Ok(Some(value.to_string())),
4068 ValueRef::Real(value) => Ok(Some(value.to_string())),
4069 ValueRef::Blob(value) => Ok(Some(
4070 String::from_utf8(value.to_vec()).context("SQL blob column was not valid UTF-8")?,
4071 )),
4072 }
4073}
4074
4075fn row_i64(row: &SqlRow<'_>, index: usize) -> Result<i64> {
4076 match row_ref(row, index)? {
4077 ValueRef::Integer(value) => Ok(value),
4078 ValueRef::Text(value) => std::str::from_utf8(value)
4079 .context("SQL text column was not valid UTF-8")?
4080 .parse::<i64>()
4081 .with_context(|| format!("failed to parse integer from SQL text column {index}")),
4082 ValueRef::Real(value) => f64_to_i64_checked(value, index),
4083 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4084 ValueRef::Blob(_) => bail!("SQL column {index} was unexpectedly a blob"),
4085 }
4086}
4087
4088fn row_hex_string(row: &SqlRow<'_>, index: usize) -> Result<String> {
4089 match row_ref(row, index)? {
4090 ValueRef::Blob(value) => Ok(bytes_to_hex(value)),
4091 ValueRef::Text(value) => Ok(std::str::from_utf8(value)
4092 .context("SQL text column was not valid UTF-8")?
4093 .to_ascii_lowercase()),
4094 ValueRef::Null => bail!("SQL column {index} was unexpectedly NULL"),
4095 ValueRef::Integer(value) => Ok(format!("{value:x}")),
4096 ValueRef::Real(value) => Ok(format!("{:x}", f64_to_i64_checked(value, index)?)),
4097 }
4098}
4099
4100fn row_opt_hex_string(row: &SqlRow<'_>, index: usize) -> Result<Option<String>> {
4101 match row_ref(row, index)? {
4102 ValueRef::Null => Ok(None),
4103 ValueRef::Blob(value) => Ok(Some(bytes_to_hex(value))),
4104 ValueRef::Text(value) => Ok(Some(
4105 std::str::from_utf8(value)
4106 .context("SQL text column was not valid UTF-8")?
4107 .to_ascii_lowercase(),
4108 )),
4109 ValueRef::Integer(value) => Ok(Some(format!("{value:x}"))),
4110 ValueRef::Real(value) => Ok(Some(format!("{:x}", f64_to_i64_checked(value, index)?))),
4111 }
4112}
4113
4114fn f64_to_i64_checked(value: f64, index: usize) -> Result<i64> {
4115 if !value.is_finite() {
4116 bail!("SQL column {index} contained a non-finite floating-point value");
4117 }
4118
4119 if value.fract() != 0.0 {
4120 bail!("SQL column {index} contained a non-integer floating-point value");
4121 }
4122
4123 if !(-9_223_372_036_854_775_808.0..=9_223_372_036_854_775_807.0).contains(&value) {
4124 bail!("SQL column {index} contained a floating-point value outside the i64 range");
4125 }
4126
4127 format!("{value:.0}")
4128 .parse::<i64>()
4129 .with_context(|| format!("failed to parse integer from SQL floating-point column {index}"))
4130}
4131
4132fn bytes_to_hex(bytes: &[u8]) -> String {
4133 let mut result = String::with_capacity(bytes.len() * 2);
4134 for byte in bytes {
4135 result.push_str(&format!("{byte:02x}"));
4136 }
4137 result
4138}
4139
4140fn query_v2_matches(
4141 connection: &Connection,
4142 query: &PackageQuery,
4143 semantics: SearchSemantics,
4144) -> Result<(Vec<V2SearchRow>, bool)> {
4145 let (where_clause, params) = build_preindexed_where_clause(query, true, semantics);
4146 let limit = source_fetch_results(query, semantics);
4147 let sql = format!(
4148 "SELECT rowid, id, name, moniker, latest_version, hash \
4149 FROM packages WHERE {where_clause} LIMIT {}",
4150 limit + 1
4151 );
4152 let mut rows = query_rows(
4153 connection,
4154 &sql,
4155 params.into_iter().map(SqlValue::Text).collect(),
4156 |row| {
4157 Ok(V2SearchRow {
4158 package_rowid: row_i64(row, 0)?,
4159 id: row_string(row, 1)?,
4160 name: row_string(row, 2)?,
4161 moniker: row_opt_string(row, 3)?,
4162 version: row_string(row, 4)?,
4163 package_hash: row_hex_string(row, 5)?,
4164 match_criteria: None,
4165 })
4166 },
4167 )?;
4168 for row in &mut rows {
4169 row.match_criteria = infer_preindexed_match_criteria_v2(connection, row, query, semantics)?;
4170 }
4171 let truncated = rows.len() > limit;
4172 rows.truncate(limit);
4173 Ok((rows, truncated))
4174}
4175
4176fn query_v1_matches(
4177 connection: &Connection,
4178 query: &PackageQuery,
4179 semantics: SearchSemantics,
4180) -> Result<(Vec<V1SearchRow>, bool)> {
4181 let (where_clause, params) = build_preindexed_where_clause(query, false, semantics);
4182 let limit = source_fetch_results(query, semantics);
4183 let sql = format!(
4184 "SELECT manifest.rowid, manifest.id, versions.version, channels.channel, ids.id, names.name, monikers.moniker \
4185 FROM manifest \
4186 JOIN ids ON manifest.id = ids.rowid \
4187 JOIN names ON manifest.name = names.rowid \
4188 LEFT JOIN monikers ON manifest.moniker = monikers.rowid \
4189 JOIN versions ON manifest.version = versions.rowid \
4190 JOIN channels ON manifest.channel = channels.rowid \
4191 WHERE {where_clause} LIMIT {}",
4192 limit + 1
4193 );
4194 let mut rows = query_rows(
4195 connection,
4196 &sql,
4197 params.into_iter().map(SqlValue::Text).collect(),
4198 |row| {
4199 Ok(V1SearchRow {
4200 manifest_rowid: row_i64(row, 0)?,
4201 package_rowid: row_i64(row, 1)?,
4202 version: row_string(row, 2)?,
4203 channel: row_string(row, 3)?,
4204 id: row_string(row, 4)?,
4205 name: row_string(row, 5)?,
4206 moniker: row_opt_string(row, 6)?,
4207 match_criteria: None,
4208 })
4209 },
4210 )?;
4211 for row in &mut rows {
4212 row.match_criteria = infer_preindexed_match_criteria_v1(connection, row, query, semantics)?;
4213 }
4214 let truncated = rows.len() > limit;
4215 rows.truncate(limit);
4216 Ok((rows, truncated))
4217}
4218
4219fn group_v1_rows(rows: Vec<V1SearchRow>) -> Vec<V1SearchRow> {
4220 let mut grouped = BTreeMap::<i64, V1SearchRow>::new();
4221
4222 for row in rows {
4223 match grouped.get(&row.package_rowid) {
4224 Some(existing)
4225 if compare_version_and_channel(&existing.version, &existing.channel, &row.version, &row.channel)
4226 != Ordering::Less => {}
4227 _ => {
4228 grouped.insert(row.package_rowid, row);
4229 }
4230 }
4231 }
4232
4233 grouped.into_values().collect()
4234}
4235
4236fn query_v1_versions(connection: &Connection, package_rowid: i64) -> Result<Vec<V1VersionRow>> {
4237 let has_hash = table_has_column(connection, "manifest", "hash")?;
4238 let hash_sql = if has_hash { "manifest.hash" } else { "NULL" };
4239 let sql = format!(
4240 "SELECT versions.version, channels.channel, manifest.pathpart, {hash_sql} \
4241 FROM manifest \
4242 JOIN versions ON manifest.version = versions.rowid \
4243 JOIN channels ON manifest.channel = channels.rowid \
4244 WHERE manifest.id = ?1"
4245 );
4246 query_rows(connection, &sql, vec![SqlValue::Integer(package_rowid)], |row| {
4247 Ok(V1VersionRow {
4248 version: row_string(row, 0)?,
4249 channel: row_string(row, 1)?,
4250 pathpart_id: row_i64(row, 2)?,
4251 manifest_hash: row_opt_hex_string(row, 3)?,
4252 })
4253 })
4254}
4255
4256fn resolve_v1_relative_path(connection: &Connection, pathpart_id: i64) -> Result<String> {
4257 let mut parts = Vec::new();
4258 let mut current = Some(pathpart_id);
4259
4260 while let Some(id) = current {
4261 let (parent, pathpart) = query_optional_value(
4262 connection,
4263 "SELECT parent, pathpart FROM pathparts WHERE rowid = ?1",
4264 vec![SqlValue::Integer(id)],
4265 |row| {
4266 Ok((
4267 row_opt_string(row, 0)?
4268 .and_then(|value| value.parse::<i64>().ok())
4269 .filter(|value| *value != 0),
4270 row_string(row, 1)?,
4271 ))
4272 },
4273 )?
4274 .ok_or_else(|| anyhow!("failed to resolve pathpart {id}"))?;
4275 parts.push(pathpart);
4276 current = parent;
4277 }
4278
4279 parts.reverse();
4280 Ok(parts.join("/"))
4281}
4282
4283fn package_version_data_relative_path(package_id: &str, package_hash: &str) -> String {
4284 format!(
4285 "packages/{}/{}/versionData.mszyml",
4286 package_id,
4287 package_hash[..package_hash.len().min(8)].to_ascii_lowercase()
4288 )
4289}
4290
4291fn can_fallback_to_v1(error: &anyhow::Error) -> bool {
4292 let message = format!("{error:#}").to_ascii_lowercase();
4293 message.contains("no such table") && message.contains("packages")
4294}
4295
4296fn table_has_column(connection: &Connection, table: &str, column: &str) -> Result<bool> {
4297 for value in query_rows(connection, &format!("PRAGMA table_info({table})"), Vec::new(), |row| {
4298 row_string(row, 1)
4299 })? {
4300 if value.eq_ignore_ascii_case(column) {
4301 return Ok(true);
4302 }
4303 }
4304
4305 Ok(false)
4306}
4307
4308fn build_preindexed_where_clause(query: &PackageQuery, v2: bool, semantics: SearchSemantics) -> (String, Vec<String>) {
4309 let rowid_column = if v2 { "packages.rowid" } else { "manifest.rowid" };
4310 let id_column = if v2 { "id" } else { "ids.id" };
4311 let name_column = if v2 { "name" } else { "names.name" };
4312 let moniker_column = if v2 { "moniker" } else { "monikers.moniker" };
4313 let exact_match = query.exact || semantics == SearchSemantics::Single;
4314 let mut params = Vec::new();
4315
4316 if let Some(value) = &query.id {
4317 return (
4318 single_field_condition(id_column, value, exact_match, &mut params),
4319 params,
4320 );
4321 }
4322 if let Some(value) = &query.name {
4323 return (
4324 single_field_condition(name_column, value, exact_match, &mut params),
4325 params,
4326 );
4327 }
4328 if let Some(value) = &query.moniker {
4329 return (
4330 single_field_condition(moniker_column, value, exact_match, &mut params),
4331 params,
4332 );
4333 }
4334 if let Some(value) = &query.tag {
4335 return (
4336 mapped_field_condition(v2, "tag", value, rowid_column, true, &mut params),
4337 params,
4338 );
4339 }
4340 if let Some(value) = &query.command {
4341 return (
4342 mapped_field_condition(v2, "command", value, rowid_column, true, &mut params),
4343 params,
4344 );
4345 }
4346 if let Some(value) = &query.query {
4347 if exact_match {
4348 let conditions = [
4349 single_field_condition(id_column, value, true, &mut params),
4350 single_field_condition(name_column, value, true, &mut params),
4351 single_field_condition(moniker_column, value, true, &mut params),
4352 ];
4353 return (format!("({})", conditions.join(" OR ")), params);
4354 }
4355
4356 let mut conditions = vec![
4357 single_field_condition(id_column, value, false, &mut params),
4358 single_field_condition(name_column, value, false, &mut params),
4359 single_field_condition(moniker_column, value, false, &mut params),
4360 ];
4361 if semantics == SearchSemantics::Many {
4362 conditions.push(mapped_field_condition(
4363 v2,
4364 "tag",
4365 value,
4366 rowid_column,
4367 false,
4368 &mut params,
4369 ));
4370 conditions.push(mapped_field_condition(
4371 v2,
4372 "command",
4373 value,
4374 rowid_column,
4375 false,
4376 &mut params,
4377 ));
4378 }
4379 return (format!("({})", conditions.join(" OR ")), params);
4380 }
4381
4382 ("1 = 1".to_owned(), Vec::new())
4383}
4384
4385fn single_field_condition(column: &str, value: &str, exact: bool, params: &mut Vec<String>) -> String {
4386 params.push(match_parameter(value, exact));
4387 format!("{column} LIKE ?{}", params.len())
4388}
4389
4390fn mapped_field_condition(
4391 v2: bool,
4392 value_name: &str,
4393 value: &str,
4394 rowid_column: &str,
4395 exact: bool,
4396 params: &mut Vec<String>,
4397) -> String {
4398 let (table_name, map_table_name, map_value_column, map_owner_column) = if v2 {
4399 (
4400 format!("{value_name}s2"),
4401 format!("{value_name}s2_map"),
4402 value_name.to_owned(),
4403 "package".to_owned(),
4404 )
4405 } else {
4406 (
4407 format!("{value_name}s"),
4408 format!("{value_name}s_map"),
4409 value_name.to_owned(),
4410 "manifest".to_owned(),
4411 )
4412 };
4413 params.push(match_parameter(value, exact));
4414 let parameter = params.len();
4415 format!(
4416 "EXISTS (SELECT 1 FROM {map_table_name} JOIN {table_name} ON \
4417 {map_table_name}.{value_name} = {table_name}.rowid \
4418 WHERE {map_table_name}.{map_owner_column} = {rowid_column} \
4419 AND {table_name}.{map_value_column} LIKE ?{parameter})"
4420 )
4421}
4422
4423fn match_parameter(value: &str, exact: bool) -> String {
4424 if exact { value.to_owned() } else { format!("%{value}%") }
4425}
4426
4427fn build_rest_search_body(
4428 query: &PackageQuery,
4429 info: &RestInformation,
4430 semantics: SearchSemantics,
4431) -> Result<JsonValue> {
4432 let mut root = serde_json::Map::new();
4433 root.insert(
4434 "MaximumResults".to_owned(),
4435 JsonValue::from(source_fetch_results(query, semantics) as u64),
4436 );
4437 let exact_match = query.exact || semantics == SearchSemantics::Single;
4438
4439 if let Some(value) = &query.query {
4440 if semantics == SearchSemantics::Single {
4441 let mut filters = vec![
4442 rest_filter("PackageIdentifier", value, true),
4443 rest_filter("PackageName", value, true),
4444 rest_filter("Moniker", value, true),
4445 ];
4446 append_required_rest_filters(&mut filters, info);
4447 root.insert("Filters".to_owned(), JsonValue::Array(filters));
4448 return Ok(JsonValue::Object(root));
4449 }
4450
4451 root.insert(
4452 "Query".to_owned(),
4453 serde_json::json!({
4454 "KeyWord": value,
4455 "MatchType": if exact_match { "Exact" } else { "Substring" },
4456 }),
4457 );
4458 }
4459
4460 let mut filters = Vec::new();
4461 if let Some(value) = &query.id {
4462 filters.push(rest_filter("PackageIdentifier", value, exact_match));
4463 }
4464 if let Some(value) = &query.name {
4465 filters.push(rest_filter("PackageName", value, exact_match));
4466 }
4467 if let Some(value) = &query.moniker {
4468 filters.push(rest_filter("Moniker", value, exact_match));
4469 }
4470 if let Some(value) = &query.tag {
4471 filters.push(rest_filter("Tag", value, true));
4472 }
4473 if let Some(value) = &query.command {
4474 filters.push(rest_filter("Command", value, true));
4475 }
4476 append_required_rest_filters(&mut filters, info);
4477
4478 if !filters.is_empty() {
4479 root.insert("Filters".to_owned(), JsonValue::Array(filters));
4480 }
4481
4482 Ok(JsonValue::Object(root))
4483}
4484
4485fn append_required_rest_filters(filters: &mut Vec<JsonValue>, info: &RestInformation) {
4486 if info
4487 .required_package_match_fields
4488 .iter()
4489 .any(|field| field.eq_ignore_ascii_case("Market"))
4490 {
4491 filters.push(rest_filter("Market", &default_market(), true));
4492 }
4493}
4494
4495fn rest_filter(field: &str, value: &str, exact: bool) -> JsonValue {
4496 serde_json::json!({
4497 "PackageMatchField": field,
4498 "RequestMatch": {
4499 "KeyWord": value,
4500 "MatchType": if exact { "Exact" } else { "Substring" },
4501 }
4502 })
4503}
4504
4505fn max_results(query: &PackageQuery) -> usize {
4506 query.count.unwrap_or(DEFAULT_MAX_RESULTS).max(1)
4507}
4508
4509fn source_fetch_results(query: &PackageQuery, semantics: SearchSemantics) -> usize {
4510 match semantics {
4511 SearchSemantics::Many => max_results(query).max(DEFAULT_MAX_RESULTS),
4512 SearchSemantics::Single => max_results(query),
4513 }
4514}
4515
4516fn infer_preindexed_match_criteria_v2(
4517 connection: &Connection,
4518 row: &V2SearchRow,
4519 query: &PackageQuery,
4520 semantics: SearchSemantics,
4521) -> Result<Option<String>> {
4522 infer_match_criteria(
4523 &row.id,
4524 &row.name,
4525 row.moniker.as_deref(),
4526 query,
4527 semantics,
4528 |value| {
4529 find_mapped_value_v2(
4530 connection,
4531 "tags2",
4532 "tags2_map",
4533 "tag",
4534 row.package_rowid,
4535 value,
4536 query.exact,
4537 )
4538 },
4539 |value| {
4540 find_mapped_value_v2(
4541 connection,
4542 "commands2",
4543 "commands2_map",
4544 "command",
4545 row.package_rowid,
4546 value,
4547 query.exact,
4548 )
4549 },
4550 )
4551}
4552
4553fn infer_preindexed_match_criteria_v1(
4554 connection: &Connection,
4555 row: &V1SearchRow,
4556 query: &PackageQuery,
4557 semantics: SearchSemantics,
4558) -> Result<Option<String>> {
4559 infer_match_criteria(
4560 &row.id,
4561 &row.name,
4562 row.moniker.as_deref(),
4563 query,
4564 semantics,
4565 |value| {
4566 find_mapped_value_v1(
4567 connection,
4568 "tags",
4569 "tags_map",
4570 "tag",
4571 row.manifest_rowid,
4572 value,
4573 query.exact,
4574 )
4575 },
4576 |value| {
4577 find_mapped_value_v1(
4578 connection,
4579 "commands",
4580 "commands_map",
4581 "command",
4582 row.manifest_rowid,
4583 value,
4584 query.exact,
4585 )
4586 },
4587 )
4588}
4589
4590fn infer_match_criteria<FTag, FCommand>(
4591 id: &str,
4592 name: &str,
4593 moniker: Option<&str>,
4594 query: &PackageQuery,
4595 semantics: SearchSemantics,
4596 find_tag: FTag,
4597 find_command: FCommand,
4598) -> Result<Option<String>>
4599where
4600 FTag: FnOnce(&str) -> Result<Option<String>>,
4601 FCommand: FnOnce(&str) -> Result<Option<String>>,
4602{
4603 if let Some(value) = &query.tag {
4604 return Ok(Some(format_match_criteria("Tag", value)));
4605 }
4606 if let Some(value) = &query.command {
4607 return Ok(Some(format_match_criteria("Command", value)));
4608 }
4609 if let Some(value) = &query.moniker {
4610 return Ok(Some(format_match_criteria("Moniker", moniker.unwrap_or(value))));
4611 }
4612 if semantics == SearchSemantics::Single {
4613 return Ok(None);
4614 }
4615 if let Some(value) = &query.query {
4616 if matches_text(id, value, query.exact) || matches_text(name, value, query.exact) {
4617 return Ok(None);
4618 }
4619 if let Some(moniker_value) = moniker.filter(|candidate| matches_text(candidate, value, query.exact)) {
4620 return Ok(Some(format_match_criteria("Moniker", moniker_value)));
4621 }
4622 if let Some(tag) = find_tag(value)? {
4623 return Ok(Some(format_match_criteria("Tag", &tag)));
4624 }
4625 if let Some(command) = find_command(value)? {
4626 return Ok(Some(format_match_criteria("Command", &command)));
4627 }
4628 }
4629 Ok(None)
4630}
4631
4632fn find_mapped_value_v2(
4633 connection: &Connection,
4634 table_name: &str,
4635 map_table_name: &str,
4636 value_name: &str,
4637 package_rowid: i64,
4638 query: &str,
4639 exact: bool,
4640) -> Result<Option<String>> {
4641 let sql = format!(
4642 "SELECT {table_name}.{value_name} FROM {map_table_name} \
4643 JOIN {table_name} ON {map_table_name}.{value_name} = {table_name}.rowid \
4644 WHERE {map_table_name}.package = ?1 AND {table_name}.{value_name} LIKE ?2"
4645 );
4646 let values = query_rows(
4647 connection,
4648 &sql,
4649 vec![
4650 SqlValue::Integer(package_rowid),
4651 SqlValue::Text(match_parameter(query, exact)),
4652 ],
4653 |row| row_string(row, 0),
4654 )?;
4655 Ok(select_best_text_match(values, query, exact))
4656}
4657
4658fn find_mapped_value_v1(
4659 connection: &Connection,
4660 table_name: &str,
4661 map_table_name: &str,
4662 value_name: &str,
4663 manifest_rowid: i64,
4664 query: &str,
4665 exact: bool,
4666) -> Result<Option<String>> {
4667 let sql = format!(
4668 "SELECT {table_name}.{value_name} FROM {map_table_name} \
4669 JOIN {table_name} ON {map_table_name}.{value_name} = {table_name}.rowid \
4670 WHERE {map_table_name}.manifest = ?1 AND {table_name}.{value_name} LIKE ?2"
4671 );
4672 let values = query_rows(
4673 connection,
4674 &sql,
4675 vec![
4676 SqlValue::Integer(manifest_rowid),
4677 SqlValue::Text(match_parameter(query, exact)),
4678 ],
4679 |row| row_string(row, 0),
4680 )?;
4681 Ok(select_best_text_match(values, query, exact))
4682}
4683
4684fn rest_match_criteria(item: &JsonValue, query: &PackageQuery, semantics: SearchSemantics) -> Option<String> {
4685 if let Some(value) = &query.tag {
4686 return Some(format_match_criteria("Tag", value));
4687 }
4688 if let Some(value) = &query.command {
4689 return Some(format_match_criteria("Command", value));
4690 }
4691 if let Some(value) = &query.moniker {
4692 return Some(format_match_criteria("Moniker", value));
4693 }
4694 if semantics == SearchSemantics::Single {
4695 return None;
4696 }
4697 let value = query.query.as_deref()?;
4698 if json_string(item, "PackageIdentifier")
4699 .as_deref()
4700 .is_some_and(|candidate| matches_text(candidate, value, query.exact))
4701 || json_string(item, "PackageName")
4702 .as_deref()
4703 .is_some_and(|candidate| matches_text(candidate, value, query.exact))
4704 {
4705 return None;
4706 }
4707 json_string(item, "Moniker")
4708 .filter(|candidate| matches_text(candidate, value, query.exact))
4709 .map(|candidate| format_match_criteria("Moniker", &candidate))
4710}
4711
4712fn format_match_criteria(field: &str, value: &str) -> String {
4713 format!("{field}: {value}")
4714}
4715
4716fn matches_text(candidate: &str, query: &str, exact: bool) -> bool {
4717 if exact {
4718 candidate.eq_ignore_ascii_case(query)
4719 } else {
4720 candidate.to_ascii_lowercase().contains(&query.to_ascii_lowercase())
4721 }
4722}
4723
4724fn search_match_sort_score(candidate: &SearchMatch, query: &PackageQuery) -> usize {
4725 let mut score = 0;
4726
4727 if let Some(value) = &query.query {
4728 score = score.max(score_text_match(&candidate.name, value, query.exact, 140, 50, 30));
4729 score = score.max(score_text_match(&candidate.id, value, query.exact, 135, 45, 25));
4730 if let Some(moniker) = candidate.moniker.as_deref() {
4731 score = score.max(score_text_match(moniker, value, query.exact, 130, 55, 35));
4732 }
4733 }
4734
4735 if let Some(value) = &query.id {
4736 score = score.max(score_text_match(&candidate.id, value, query.exact, 220, 200, 180));
4737 }
4738 if let Some(value) = &query.name {
4739 score = score.max(score_text_match(&candidate.name, value, query.exact, 210, 190, 170));
4740 }
4741 if let Some(value) = &query.moniker
4742 && let Some(moniker) = candidate.moniker.as_deref()
4743 {
4744 score = score.max(score_text_match(moniker, value, query.exact, 205, 185, 165));
4745 }
4746 if let Some(value) = &query.tag
4747 && let Some(("Tag", matched_value)) = candidate.match_criteria.as_deref().and_then(parse_match_criteria)
4748 {
4749 score = score.max(score_text_match(matched_value, value, query.exact, 160, 150, 140));
4750 }
4751 if let Some(value) = &query.command
4752 && let Some(("Command", matched_value)) = candidate.match_criteria.as_deref().and_then(parse_match_criteria)
4753 {
4754 score = score.max(score_text_match(matched_value, value, query.exact, 155, 145, 135));
4755 }
4756
4757 if matches!(candidate.source_kind, SourceKind::PreIndexed) {
4758 score += 5;
4759 }
4760 if search_match_has_unknown_version(candidate) {
4761 score = score.saturating_sub(10);
4762 }
4763
4764 score
4765}
4766
4767fn search_match_has_unknown_version(candidate: &SearchMatch) -> bool {
4768 candidate
4769 .version
4770 .as_deref()
4771 .is_some_and(|value| value.eq_ignore_ascii_case("Unknown"))
4772}
4773
4774fn resolve_manifest_path(manifest_path: &Path) -> Result<PathBuf> {
4775 if manifest_path.is_file() {
4776 return Ok(manifest_path.to_path_buf());
4777 }
4778
4779 if !manifest_path.is_dir() {
4780 bail!("Manifest path not found: {}", manifest_path.display());
4781 }
4782
4783 let candidate = fs::read_dir(manifest_path)?
4784 .filter_map(|entry| entry.ok())
4785 .map(|entry| entry.path())
4786 .filter(|path| {
4787 path.extension()
4788 .and_then(|value| value.to_str())
4789 .is_some_and(|ext| ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml"))
4790 })
4791 .min();
4792
4793 candidate.ok_or_else(|| anyhow!("No manifest file found under: {}", manifest_path.display()))
4794}
4795
4796fn score_text_match(
4797 candidate: &str,
4798 query: &str,
4799 exact: bool,
4800 exact_score: usize,
4801 prefix_score: usize,
4802 substring_score: usize,
4803) -> usize {
4804 if candidate.eq_ignore_ascii_case(query) {
4805 return exact_score;
4806 }
4807 if exact {
4808 return 0;
4809 }
4810
4811 let candidate_lower = candidate.to_ascii_lowercase();
4812 let query_lower = query.to_ascii_lowercase();
4813 if candidate_lower.starts_with(&query_lower) {
4814 prefix_score
4815 } else if candidate_lower.contains(&query_lower) {
4816 substring_score
4817 } else {
4818 0
4819 }
4820}
4821
4822fn select_best_text_match<I>(values: I, query: &str, exact: bool) -> Option<String>
4823where
4824 I: IntoIterator<Item = String>,
4825{
4826 values
4827 .into_iter()
4828 .filter_map(|candidate| {
4829 let score = score_text_match(&candidate, query, exact, 3, 2, 1);
4830 (score > 0).then_some((score, candidate))
4831 })
4832 .max_by(|(left_score, left_value), (right_score, right_value)| {
4833 left_score
4834 .cmp(right_score)
4835 .then_with(|| right_value.len().cmp(&left_value.len()))
4836 .then_with(|| right_value.to_ascii_lowercase().cmp(&left_value.to_ascii_lowercase()))
4837 })
4838 .map(|(_, candidate)| candidate)
4839}
4840
4841fn parse_match_criteria(criteria: &str) -> Option<(&str, &str)> {
4842 let (field, value) = criteria.split_once(": ")?;
4843 Some((field, value))
4844}
4845
4846fn parse_rest_versions(item: &JsonValue) -> Result<Vec<VersionKey>> {
4847 let versions = item
4848 .get("Versions")
4849 .and_then(JsonValue::as_array)
4850 .ok_or_else(|| anyhow!("REST search result missing Versions"))?;
4851 let mut result = Vec::new();
4852 for version in versions {
4853 let value = json_string(version, "PackageVersion")
4854 .ok_or_else(|| anyhow!("REST version entry missing PackageVersion"))?;
4855 let channel = json_string(version, "Channel").unwrap_or_default();
4856 result.push(VersionKey {
4857 version: value,
4858 channel,
4859 });
4860 }
4861 Ok(result)
4862}
4863
4864fn parse_yaml_manifest_bundle(bytes: &[u8]) -> Result<(Manifest, JsonValue)> {
4865 let mut merged = YamlMapping::new();
4866 let mut documents = Vec::new();
4867 for document in serde_yaml::Deserializer::from_slice(bytes) {
4868 let value = YamlValue::deserialize(document).context("failed to deserialize YAML document")?;
4869 documents.push(serde_json::to_value(&value).context("failed to convert YAML manifest document")?);
4870 if let Some(mapping) = value.as_mapping() {
4871 merge_yaml_mapping(&mut merged, mapping);
4872 }
4873 }
4874
4875 let id = yaml_string_from_root(&merged, "PackageIdentifier")
4876 .ok_or_else(|| anyhow!("manifest missing PackageIdentifier"))?;
4877 let version =
4878 yaml_string_from_root(&merged, "PackageVersion").ok_or_else(|| anyhow!("manifest missing PackageVersion"))?;
4879 let name = yaml_localized_string(&merged, "PackageName").ok_or_else(|| anyhow!("manifest missing PackageName"))?;
4880
4881 let installers = parse_yaml_installers(&merged);
4882
4883 Ok((
4884 Manifest {
4885 id,
4886 name,
4887 version,
4888 channel: yaml_string_from_root(&merged, "Channel").unwrap_or_default(),
4889 publisher: yaml_localized_string(&merged, "Publisher"),
4890 description: yaml_localized_string(&merged, "Description")
4891 .or_else(|| yaml_localized_string(&merged, "ShortDescription")),
4892 moniker: yaml_string_from_root(&merged, "Moniker"),
4893 package_url: yaml_localized_string(&merged, "PackageUrl"),
4894 publisher_url: yaml_localized_string(&merged, "PublisherUrl"),
4895 publisher_support_url: yaml_localized_string(&merged, "PublisherSupportUrl"),
4896 license: yaml_localized_string(&merged, "License"),
4897 license_url: yaml_localized_string(&merged, "LicenseUrl"),
4898 privacy_url: yaml_localized_string(&merged, "PrivacyUrl"),
4899 author: yaml_localized_string(&merged, "Author"),
4900 copyright: yaml_localized_string(&merged, "Copyright"),
4901 copyright_url: yaml_localized_string(&merged, "CopyrightUrl"),
4902 release_notes: yaml_localized_string(&merged, "ReleaseNotes"),
4903 release_notes_url: yaml_localized_string(&merged, "ReleaseNotesUrl"),
4904 tags: yaml_string_list(&merged, "Tags"),
4905 agreements: yaml_agreement_list(&merged),
4906 package_dependencies: yaml_package_dependencies(&merged),
4907 documentation: yaml_documentation_list(&merged),
4908 installers,
4909 },
4910 collapse_structured_document(&JsonValue::Array(documents)),
4911 ))
4912}
4913
4914fn parse_yaml_manifest(bytes: &[u8]) -> Result<Manifest> {
4915 parse_yaml_manifest_bundle(bytes).map(|(manifest, _)| manifest)
4916}
4917
4918fn parse_rest_manifest(bytes: &[u8], package_id: &str, version: &str, channel: &str) -> Result<(Manifest, JsonValue)> {
4919 let root = serde_json::from_slice::<JsonValue>(bytes).context("failed to deserialize REST manifest JSON")?;
4920 let data = root
4921 .get("Data")
4922 .ok_or_else(|| anyhow!("REST manifest response missing Data"))?;
4923 let versions = data
4924 .get("Versions")
4925 .and_then(JsonValue::as_array)
4926 .ok_or_else(|| anyhow!("REST manifest response missing Versions"))?;
4927 let selected = versions
4928 .iter()
4929 .find(|item| {
4930 json_string(item, "PackageVersion").as_deref() == Some(version)
4931 && json_string(item, "Channel").unwrap_or_default() == channel
4932 })
4933 .or_else(|| versions.first())
4934 .ok_or_else(|| anyhow!("REST manifest response did not contain a version payload"))?;
4935
4936 let default_locale = selected
4937 .get("DefaultLocale")
4938 .ok_or_else(|| anyhow!("REST manifest response missing DefaultLocale"))?;
4939 let name = json_string(default_locale, "PackageName")
4940 .ok_or_else(|| anyhow!("REST manifest response missing PackageName"))?;
4941 let installer_switch_defaults = json_installer_switches(selected);
4942 let top_platforms = json_string_list(selected, "Platform");
4943 let top_minimum_os_version = json_string(selected, "MinimumOSVersion");
4944
4945 let installers = selected
4946 .get("Installers")
4947 .and_then(JsonValue::as_array)
4948 .map(|items| {
4949 items
4950 .iter()
4951 .map(|item| Installer {
4952 platforms: {
4953 let platforms = json_string_list(item, "Platform");
4954 if platforms.is_empty() {
4955 top_platforms.clone()
4956 } else {
4957 platforms
4958 }
4959 },
4960 minimum_os_version: json_string(item, "MinimumOSVersion")
4961 .or_else(|| top_minimum_os_version.clone()),
4962 architecture: json_string(item, "Architecture"),
4963 installer_type: json_string(item, "InstallerType"),
4964 url: json_string(item, "InstallerUrl"),
4965 sha256: json_string(item, "InstallerSha256"),
4966 product_code: json_string(item, "ProductCode"),
4967 locale: json_string(item, "InstallerLocale"),
4968 scope: json_string(item, "Scope"),
4969 release_date: json_string(item, "ReleaseDate"),
4970 package_family_name: json_string(item, "PackageFamilyName"),
4971 upgrade_code: json_string(item, "UpgradeCode"),
4972 switches: json_installer_switches(item).with_fallback(&installer_switch_defaults),
4973 commands: json_string_list(item, "Commands"),
4974 package_dependencies: json_package_dependencies(item),
4975 })
4976 .collect::<Vec<_>>()
4977 })
4978 .unwrap_or_default();
4979
4980 let manifest = Manifest {
4981 id: package_id.to_owned(),
4982 name,
4983 version: version.to_owned(),
4984 channel: channel.to_owned(),
4985 publisher: json_string(default_locale, "Publisher"),
4986 description: json_string(default_locale, "Description")
4987 .or_else(|| json_string(default_locale, "ShortDescription")),
4988 moniker: json_string(default_locale, "Moniker"),
4989 package_url: json_string(default_locale, "PackageUrl"),
4990 publisher_url: json_string(default_locale, "PublisherUrl"),
4991 publisher_support_url: json_string(default_locale, "PublisherSupportUrl"),
4992 license: json_string(default_locale, "License"),
4993 license_url: json_string(default_locale, "LicenseUrl"),
4994 privacy_url: json_string(default_locale, "PrivacyUrl"),
4995 author: json_string(default_locale, "Author"),
4996 copyright: json_string(default_locale, "Copyright"),
4997 copyright_url: json_string(default_locale, "CopyrightUrl"),
4998 release_notes: json_string(default_locale, "ReleaseNotes"),
4999 release_notes_url: json_string(default_locale, "ReleaseNotesUrl"),
5000 tags: json_string_list(default_locale, "Tags"),
5001 agreements: json_agreement_list(default_locale),
5002 package_dependencies: json_package_dependencies(selected),
5003 documentation: json_documentation_list(default_locale),
5004 installers,
5005 };
5006
5007 Ok((
5008 manifest,
5009 build_rest_manifest_documents(data, selected, default_locale, package_id, version, channel),
5010 ))
5011}
5012
5013fn collapse_structured_document(document: &JsonValue) -> JsonValue {
5014 match document {
5015 JsonValue::Array(documents) if documents.len() == 1 && is_merged_manifest_document(&documents[0]) => {
5016 merge_manifest_documents(&split_merged_manifest_document(&documents[0]))
5017 }
5018 JsonValue::Array(documents) => merge_manifest_documents(documents),
5019 JsonValue::Object(_) if is_merged_manifest_document(document) => {
5020 merge_manifest_documents(&split_merged_manifest_document(document))
5021 }
5022 _ => document.clone(),
5023 }
5024}
5025
5026fn collapse_structured_documents(documents: &[JsonValue]) -> Vec<JsonValue> {
5027 documents.iter().map(collapse_structured_document).collect()
5028}
5029
5030fn merge_manifest_documents(documents: &[JsonValue]) -> JsonValue {
5031 let version = documents.iter().find(|document| {
5032 document
5033 .get("ManifestType")
5034 .and_then(JsonValue::as_str)
5035 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("version"))
5036 });
5037 let default_locale = documents.iter().find(|document| {
5038 document
5039 .get("ManifestType")
5040 .and_then(JsonValue::as_str)
5041 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("defaultLocale"))
5042 });
5043 let installer = documents.iter().find(|document| {
5044 document
5045 .get("ManifestType")
5046 .and_then(JsonValue::as_str)
5047 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("installer"))
5048 });
5049
5050 let mut singleton = JsonMap::new();
5051 copy_manifest_keys(&mut singleton, version, &["PackageIdentifier", "PackageVersion"]);
5052 copy_all_manifest_keys_except(&mut singleton, default_locale, &["ManifestType", "ManifestVersion"]);
5053 copy_all_manifest_keys_except(&mut singleton, installer, &["ManifestType", "ManifestVersion"]);
5054
5055 if !singleton.contains_key("PackageLocale")
5056 && let Some(package_locale) = default_locale
5057 .and_then(|document| json_string(document, "PackageLocale"))
5058 .or_else(|| version.and_then(|document| json_string(document, "DefaultLocale")))
5059 {
5060 singleton.insert("PackageLocale".to_owned(), JsonValue::String(package_locale));
5061 }
5062
5063 singleton.insert("ManifestType".to_owned(), JsonValue::String("singleton".to_owned()));
5064 singleton.insert(
5065 "ManifestVersion".to_owned(),
5066 JsonValue::String(
5067 installer
5068 .and_then(|document| json_string(document, "ManifestVersion"))
5069 .or_else(|| default_locale.and_then(|document| json_string(document, "ManifestVersion")))
5070 .or_else(|| version.and_then(|document| json_string(document, "ManifestVersion")))
5071 .unwrap_or_else(|| "1.10.0".to_owned()),
5072 ),
5073 );
5074 singleton.remove("DefaultLocale");
5075
5076 JsonValue::Object(singleton)
5077}
5078
5079fn is_merged_manifest_document(document: &JsonValue) -> bool {
5080 document
5081 .get("ManifestType")
5082 .and_then(JsonValue::as_str)
5083 .is_some_and(|manifest_type| manifest_type.eq_ignore_ascii_case("merged"))
5084}
5085
5086fn split_merged_manifest_document(merged: &JsonValue) -> Vec<JsonValue> {
5087 let package_identifier = json_string(merged, "PackageIdentifier").unwrap_or_default();
5088 let package_version = json_string(merged, "PackageVersion").unwrap_or_default();
5089 let package_locale = json_string(merged, "PackageLocale").unwrap_or_else(|| "en-US".to_owned());
5090 let manifest_version = json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned());
5091
5092 let version_document = serde_json::json!({
5093 "PackageIdentifier": package_identifier,
5094 "PackageVersion": package_version,
5095 "DefaultLocale": package_locale,
5096 "ManifestType": "version",
5097 "ManifestVersion": manifest_version,
5098 });
5099
5100 let mut default_locale_document = project_manifest_document(
5101 merged,
5102 &[
5103 "PackageIdentifier",
5104 "PackageVersion",
5105 "PackageLocale",
5106 "Publisher",
5107 "PublisherUrl",
5108 "PublisherSupportUrl",
5109 "PrivacyUrl",
5110 "Author",
5111 "PackageName",
5112 "PackageUrl",
5113 "License",
5114 "LicenseUrl",
5115 "Copyright",
5116 "CopyrightUrl",
5117 "ShortDescription",
5118 "Description",
5119 "Moniker",
5120 "Tags",
5121 "Agreements",
5122 "ReleaseNotes",
5123 "ReleaseNotesUrl",
5124 "PurchaseUrl",
5125 "InstallationNotes",
5126 "Documentations",
5127 "Icons",
5128 ],
5129 );
5130 default_locale_document.insert(
5131 "PackageIdentifier".to_owned(),
5132 JsonValue::String(json_string(merged, "PackageIdentifier").unwrap_or_default()),
5133 );
5134 default_locale_document.insert(
5135 "PackageVersion".to_owned(),
5136 JsonValue::String(json_string(merged, "PackageVersion").unwrap_or_default()),
5137 );
5138 default_locale_document.insert(
5139 "PackageLocale".to_owned(),
5140 JsonValue::String(json_string(merged, "PackageLocale").unwrap_or_else(|| "en-US".to_owned())),
5141 );
5142 default_locale_document.insert("ManifestType".to_owned(), JsonValue::String("defaultLocale".to_owned()));
5143 default_locale_document.insert(
5144 "ManifestVersion".to_owned(),
5145 JsonValue::String(json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned())),
5146 );
5147
5148 let mut installer_document = project_manifest_document(
5149 merged,
5150 &[
5151 "PackageIdentifier",
5152 "PackageVersion",
5153 "Channel",
5154 "InstallerLocale",
5155 "Platform",
5156 "MinimumOSVersion",
5157 "InstallerType",
5158 "NestedInstallerType",
5159 "NestedInstallerFiles",
5160 "Scope",
5161 "InstallModes",
5162 "InstallerSwitches",
5163 "InstallerSuccessCodes",
5164 "ExpectedReturnCodes",
5165 "UpgradeBehavior",
5166 "Commands",
5167 "Protocols",
5168 "FileExtensions",
5169 "Dependencies",
5170 "PackageFamilyName",
5171 "ProductCode",
5172 "Capabilities",
5173 "RestrictedCapabilities",
5174 "Markets",
5175 "InstallerAbortsTerminal",
5176 "ReleaseDate",
5177 "InstallLocationRequired",
5178 "RequireExplicitUpgrade",
5179 "DisplayInstallWarnings",
5180 "UnsupportedOSArchitectures",
5181 "UnsupportedArguments",
5182 "AppsAndFeaturesEntries",
5183 "ElevationRequirement",
5184 "InstallationMetadata",
5185 "DownloadCommandProhibited",
5186 "RepairBehavior",
5187 "ArchiveBinariesDependOnPath",
5188 "Authentication",
5189 "Installers",
5190 ],
5191 );
5192 installer_document.insert(
5193 "PackageIdentifier".to_owned(),
5194 JsonValue::String(json_string(merged, "PackageIdentifier").unwrap_or_default()),
5195 );
5196 installer_document.insert(
5197 "PackageVersion".to_owned(),
5198 JsonValue::String(json_string(merged, "PackageVersion").unwrap_or_default()),
5199 );
5200 installer_document.insert("ManifestType".to_owned(), JsonValue::String("installer".to_owned()));
5201 installer_document.insert(
5202 "ManifestVersion".to_owned(),
5203 JsonValue::String(json_string(merged, "ManifestVersion").unwrap_or_else(|| "1.10.0".to_owned())),
5204 );
5205
5206 vec![
5207 version_document,
5208 JsonValue::Object(default_locale_document),
5209 JsonValue::Object(installer_document),
5210 ]
5211}
5212
5213fn project_manifest_document(source: &JsonValue, keys: &[&str]) -> JsonMap<String, JsonValue> {
5214 let mut result = JsonMap::new();
5215 if let Some(source) = source.as_object() {
5216 for key in keys {
5217 if let Some(value) = source.get(*key) {
5218 result.insert((*key).to_owned(), value.clone());
5219 }
5220 }
5221 }
5222
5223 result
5224}
5225
5226fn copy_manifest_keys(target: &mut JsonMap<String, JsonValue>, source: Option<&JsonValue>, keys: &[&str]) {
5227 if let Some(source) = source.and_then(JsonValue::as_object) {
5228 for key in keys {
5229 if let Some(value) = source.get(*key) {
5230 target.insert((*key).to_owned(), value.clone());
5231 }
5232 }
5233 }
5234}
5235
5236fn copy_all_manifest_keys_except(
5237 target: &mut JsonMap<String, JsonValue>,
5238 source: Option<&JsonValue>,
5239 excluded_keys: &[&str],
5240) {
5241 if let Some(source) = source.and_then(JsonValue::as_object) {
5242 for (key, value) in source {
5243 if !excluded_keys.iter().any(|excluded| key.eq_ignore_ascii_case(excluded)) {
5244 target.insert(key.clone(), value.clone());
5245 }
5246 }
5247 }
5248}
5249
5250fn build_rest_manifest_documents(
5251 data: &JsonValue,
5252 installer_source: &JsonValue,
5253 default_locale: &JsonValue,
5254 package_id: &str,
5255 version: &str,
5256 channel: &str,
5257) -> JsonValue {
5258 let package_identifier = json_string(data, "PackageIdentifier").unwrap_or_else(|| package_id.to_owned());
5259 let package_locale = json_string(default_locale, "PackageLocale").unwrap_or_else(|| "en-US".to_owned());
5260 let manifest_version = json_string(installer_source, "ManifestVersion")
5261 .or_else(|| json_string(default_locale, "ManifestVersion"))
5262 .or_else(|| json_string(data, "ManifestVersion"))
5263 .unwrap_or_else(|| "1.10.0".to_owned());
5264
5265 let mut version_document = JsonMap::new();
5266 version_document.insert(
5267 "PackageIdentifier".to_owned(),
5268 JsonValue::String(package_identifier.clone()),
5269 );
5270 version_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5271 version_document.insert("DefaultLocale".to_owned(), JsonValue::String(package_locale.clone()));
5272 version_document.insert("ManifestType".to_owned(), JsonValue::String("version".to_owned()));
5273 version_document.insert(
5274 "ManifestVersion".to_owned(),
5275 JsonValue::String(manifest_version.clone()),
5276 );
5277
5278 let mut default_locale_document = default_locale.as_object().cloned().unwrap_or_else(JsonMap::new);
5279 default_locale_document.insert(
5280 "PackageIdentifier".to_owned(),
5281 JsonValue::String(package_identifier.clone()),
5282 );
5283 default_locale_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5284 default_locale_document.insert("PackageLocale".to_owned(), JsonValue::String(package_locale));
5285 default_locale_document.insert("ManifestType".to_owned(), JsonValue::String("defaultLocale".to_owned()));
5286 default_locale_document.insert(
5287 "ManifestVersion".to_owned(),
5288 JsonValue::String(manifest_version.clone()),
5289 );
5290
5291 let mut installer_document = installer_source.as_object().cloned().unwrap_or_else(JsonMap::new);
5292 installer_document.remove("DefaultLocale");
5293 installer_document.remove("Locales");
5294 installer_document.insert("PackageIdentifier".to_owned(), JsonValue::String(package_identifier));
5295 installer_document.insert("PackageVersion".to_owned(), JsonValue::String(version.to_owned()));
5296 if !channel.is_empty() {
5297 installer_document.insert("Channel".to_owned(), JsonValue::String(channel.to_owned()));
5298 }
5299 installer_document.insert("ManifestType".to_owned(), JsonValue::String("installer".to_owned()));
5300 installer_document.insert("ManifestVersion".to_owned(), JsonValue::String(manifest_version));
5301
5302 let documents = vec![
5303 JsonValue::Object(version_document),
5304 JsonValue::Object(default_locale_document),
5305 JsonValue::Object(installer_document),
5306 ];
5307
5308 merge_manifest_documents(&documents)
5309}
5310
5311fn parse_yaml_installers(root: &YamlMapping) -> Vec<Installer> {
5312 let base = installer_defaults(root);
5313 let base_switches = yaml_installer_switches(root);
5314 if let Some(items) = root.get(YamlValue::from("Installers")).and_then(YamlValue::as_sequence) {
5315 let installers = items
5316 .iter()
5317 .filter_map(YamlValue::as_mapping)
5318 .map(|item| {
5319 let mut merged = base.clone();
5320 merge_yaml_mapping(&mut merged, item);
5321 let switches = yaml_installer_switches(item).with_fallback(&base_switches);
5322 installer_from_yaml(&merged, switches)
5323 })
5324 .collect::<Vec<_>>();
5325 if !installers.is_empty() {
5326 return installers;
5327 }
5328 }
5329
5330 if base.is_empty() {
5331 Vec::new()
5332 } else {
5333 vec![installer_from_yaml(&base, base_switches)]
5334 }
5335}
5336
5337fn installer_defaults(root: &YamlMapping) -> YamlMapping {
5338 let keys = [
5339 "Architecture",
5340 "InstallerType",
5341 "InstallerUrl",
5342 "InstallerSha256",
5343 "ProductCode",
5344 "InstallerLocale",
5345 "Platform",
5346 "MinimumOSVersion",
5347 "Scope",
5348 "ReleaseDate",
5349 "PackageFamilyName",
5350 "UpgradeCode",
5351 "Commands",
5352 ];
5353 let mut defaults = YamlMapping::new();
5354 for key in keys {
5355 if let Some(value) = root.get(YamlValue::from(key)) {
5356 defaults.insert(YamlValue::from(key), value.clone());
5357 }
5358 }
5359 defaults
5360}
5361
5362fn installer_from_yaml(root: &YamlMapping, switches: InstallerSwitches) -> Installer {
5363 Installer {
5364 architecture: yaml_string(root, "Architecture"),
5365 installer_type: yaml_string(root, "InstallerType"),
5366 url: yaml_string(root, "InstallerUrl"),
5367 sha256: yaml_string(root, "InstallerSha256"),
5368 product_code: yaml_string(root, "ProductCode"),
5369 locale: yaml_string(root, "InstallerLocale"),
5370 scope: yaml_string(root, "Scope"),
5371 release_date: yaml_string(root, "ReleaseDate"),
5372 package_family_name: yaml_string(root, "PackageFamilyName"),
5373 upgrade_code: yaml_string(root, "UpgradeCode"),
5374 platforms: yaml_string_list(root, "Platform"),
5375 minimum_os_version: yaml_string(root, "MinimumOSVersion"),
5376 switches,
5377 commands: yaml_string_list(root, "Commands"),
5378 package_dependencies: yaml_package_dependencies(root),
5379 }
5380}
5381
5382fn yaml_installer_switches(root: &YamlMapping) -> InstallerSwitches {
5383 let mapping = root
5384 .get(YamlValue::from("InstallerSwitches"))
5385 .or_else(|| root.get(YamlValue::from("Switches")))
5386 .and_then(YamlValue::as_mapping);
5387 yaml_installer_switches_from_mapping(mapping)
5388}
5389
5390fn yaml_installer_switches_from_mapping(mapping: Option<&YamlMapping>) -> InstallerSwitches {
5391 let string = |key: &str| mapping.and_then(|mapping| mapping.get(YamlValue::from(key)).and_then(yaml_scalar_string));
5392
5393 InstallerSwitches {
5394 silent: string("Silent"),
5395 silent_with_progress: string("SilentWithProgress"),
5396 interactive: string("Interactive"),
5397 custom: string("Custom"),
5398 log: string("Log"),
5399 install_location: string("InstallLocation"),
5400 }
5401}
5402
5403fn yaml_localized_string(root: &YamlMapping, key: &str) -> Option<String> {
5404 yaml_string(root, key).or_else(|| {
5405 root.get(YamlValue::from("DefaultLocale"))
5406 .and_then(YamlValue::as_mapping)
5407 .and_then(|mapping| yaml_string(mapping, key))
5408 })
5409}
5410
5411fn yaml_string_from_root(root: &YamlMapping, key: &str) -> Option<String> {
5412 yaml_string(root, key)
5413}
5414
5415fn yaml_string(root: &YamlMapping, key: &str) -> Option<String> {
5416 root.get(YamlValue::from(key)).and_then(yaml_scalar_string)
5417}
5418
5419fn yaml_scalar_string(value: &YamlValue) -> Option<String> {
5420 match value {
5421 YamlValue::Null => None,
5422 YamlValue::Bool(value) => Some(value.to_string()),
5423 YamlValue::Number(value) => Some(value.to_string()),
5424 YamlValue::String(value) => Some(value.clone()),
5425 YamlValue::Tagged(tagged) => yaml_scalar_string(&tagged.value),
5426 YamlValue::Sequence(_) | YamlValue::Mapping(_) => None,
5427 }
5428}
5429
5430fn yaml_string_list(root: &YamlMapping, key: &str) -> Vec<String> {
5431 root.get(YamlValue::from(key))
5432 .and_then(YamlValue::as_sequence)
5433 .map(|items| items.iter().filter_map(YamlValue::as_str).map(str::to_owned).collect())
5434 .unwrap_or_default()
5435}
5436
5437fn yaml_documentation_list(root: &YamlMapping) -> Vec<Documentation> {
5438 root.get(YamlValue::from("Documentations"))
5439 .and_then(YamlValue::as_sequence)
5440 .map(|items| {
5441 items
5442 .iter()
5443 .filter_map(YamlValue::as_mapping)
5444 .filter_map(|item| {
5445 let url = yaml_string(item, "DocumentUrl")?;
5446 Some(Documentation {
5447 label: yaml_string(item, "DocumentLabel"),
5448 url,
5449 })
5450 })
5451 .collect()
5452 })
5453 .unwrap_or_default()
5454}
5455
5456fn yaml_agreement_list(root: &YamlMapping) -> Vec<PackageAgreement> {
5457 root.get(YamlValue::from("Agreements"))
5458 .and_then(YamlValue::as_sequence)
5459 .map(|items| {
5460 items
5461 .iter()
5462 .filter_map(YamlValue::as_mapping)
5463 .map(|item| PackageAgreement {
5464 label: yaml_string(item, "AgreementLabel"),
5465 text: yaml_string(item, "Agreement"),
5466 url: yaml_string(item, "AgreementUrl"),
5467 })
5468 .collect()
5469 })
5470 .unwrap_or_default()
5471}
5472
5473fn yaml_package_dependencies(root: &YamlMapping) -> Vec<String> {
5474 root.get(YamlValue::from("Dependencies"))
5475 .and_then(YamlValue::as_mapping)
5476 .and_then(|deps| deps.get(YamlValue::from("PackageDependencies")))
5477 .and_then(YamlValue::as_sequence)
5478 .map(|items| {
5479 items
5480 .iter()
5481 .filter_map(YamlValue::as_mapping)
5482 .filter_map(|item| yaml_string(item, "PackageIdentifier"))
5483 .collect()
5484 })
5485 .unwrap_or_default()
5486}
5487
5488fn json_string(value: &JsonValue, key: &str) -> Option<String> {
5489 value.get(key).and_then(JsonValue::as_str).map(str::to_owned)
5490}
5491
5492fn json_string_list(value: &JsonValue, key: &str) -> Vec<String> {
5493 value
5494 .get(key)
5495 .and_then(JsonValue::as_array)
5496 .map(|items| items.iter().filter_map(JsonValue::as_str).map(str::to_owned).collect())
5497 .unwrap_or_default()
5498}
5499
5500fn json_documentation_list(value: &JsonValue) -> Vec<Documentation> {
5501 value
5502 .get("Documentations")
5503 .and_then(JsonValue::as_array)
5504 .map(|items| {
5505 items
5506 .iter()
5507 .filter_map(|item| {
5508 let url = json_string(item, "DocumentUrl")?;
5509 Some(Documentation {
5510 label: json_string(item, "DocumentLabel"),
5511 url,
5512 })
5513 })
5514 .collect()
5515 })
5516 .unwrap_or_default()
5517}
5518
5519fn json_agreement_list(value: &JsonValue) -> Vec<PackageAgreement> {
5520 value
5521 .get("Agreements")
5522 .and_then(JsonValue::as_array)
5523 .map(|items| {
5524 items
5525 .iter()
5526 .map(|item| PackageAgreement {
5527 label: json_string(item, "AgreementLabel"),
5528 text: json_string(item, "Agreement"),
5529 url: json_string(item, "AgreementUrl"),
5530 })
5531 .collect()
5532 })
5533 .unwrap_or_default()
5534}
5535
5536fn json_package_dependencies(value: &JsonValue) -> Vec<String> {
5537 value
5538 .get("Dependencies")
5539 .and_then(JsonValue::as_object)
5540 .and_then(|deps| deps.get("PackageDependencies"))
5541 .and_then(JsonValue::as_array)
5542 .map(|items| {
5543 items
5544 .iter()
5545 .filter_map(|item| json_string(item, "PackageIdentifier"))
5546 .collect()
5547 })
5548 .unwrap_or_default()
5549}
5550
5551fn json_installer_switches(value: &JsonValue) -> InstallerSwitches {
5552 let switches = value
5553 .get("InstallerSwitches")
5554 .or_else(|| value.get("Switches"))
5555 .and_then(JsonValue::as_object);
5556
5557 let string = |key: &str| {
5558 switches
5559 .and_then(|switches| switches.get(key))
5560 .and_then(JsonValue::as_str)
5561 .map(str::to_owned)
5562 };
5563
5564 InstallerSwitches {
5565 silent: string("Silent"),
5566 silent_with_progress: string("SilentWithProgress"),
5567 interactive: string("Interactive"),
5568 custom: string("Custom"),
5569 log: string("Log"),
5570 install_location: string("InstallLocation"),
5571 }
5572}
5573
5574fn merge_yaml_mapping(target: &mut YamlMapping, source: &YamlMapping) {
5575 for (key, value) in source {
5576 target.insert(key.clone(), value.clone());
5577 }
5578}
5579
5580fn extract_zip_member(bytes: &[u8], member_name: &str) -> Result<Vec<u8>> {
5581 let reader = Cursor::new(bytes.to_vec());
5582 let mut archive = ZipArchive::new(reader).context("failed to read zip payload")?;
5583 let mut file = archive
5584 .by_name(member_name)
5585 .with_context(|| format!("zip payload missing {member_name}"))?;
5586 let mut output = Vec::new();
5587 file.read_to_end(&mut output)
5588 .with_context(|| format!("failed to read {member_name} from zip payload"))?;
5589 Ok(output)
5590}
5591
5592fn decompress_mszyml(bytes: &[u8]) -> Result<String> {
5593 if let Ok(output) = decompress_mszyml_payload(if bytes.starts_with(b"CK") { &bytes[2..] } else { bytes }) {
5594 return Ok(output);
5595 }
5596
5597 if !bytes.starts_with(b"CK")
5598 && let Some(offset) = bytes.windows(2).position(|window| window == b"CK")
5599 {
5600 return decompress_mszyml_payload(&bytes[offset + 2..]);
5601 }
5602
5603 decompress_mszyml_payload(bytes).context("failed to decompress MSZIP payload")
5604}
5605
5606fn decompress_mszyml_payload(payload: &[u8]) -> Result<String> {
5607 let mut decoder = flate2::read::DeflateDecoder::new(payload);
5608 let mut output = String::new();
5609 decoder.read_to_string(&mut output)?;
5610 Ok(output)
5611}
5612
5613fn cache_control_max_age(response: &Response) -> u64 {
5614 response
5615 .headers()
5616 .get(reqwest::header::CACHE_CONTROL)
5617 .and_then(|value| value.to_str().ok())
5618 .and_then(|header| {
5619 header.split(',').find_map(|part| {
5620 let trimmed = part.trim();
5621 trimmed
5622 .strip_prefix("max-age=")
5623 .and_then(|value| value.parse::<u64>().ok())
5624 })
5625 })
5626 .unwrap_or(60)
5627}
5628
5629fn choose_contract(server_versions: &[String]) -> Option<&'static str> {
5630 REST_SUPPORTED_CONTRACTS.iter().copied().find(|candidate| {
5631 let (major, minor) = major_minor(candidate);
5632 server_versions.iter().any(|version| {
5633 let (server_major, server_minor) = major_minor(version);
5634 server_major == major && server_minor == minor
5635 })
5636 })
5637}
5638
5639fn major_minor(version: &str) -> (String, String) {
5640 let mut parts = version.split('.');
5641 (
5642 parts.next().unwrap_or_default().to_owned(),
5643 parts.next().unwrap_or_default().to_owned(),
5644 )
5645}
5646
5647fn default_market() -> String {
5648 std::env::var("WINGET_RS_MARKET").unwrap_or_else(|_| DEFAULT_MARKET.to_owned())
5649}
5650
5651fn select_installer(installers: &[Installer], query: &PackageQuery) -> Option<Installer> {
5652 let requested_locale = query.locale.as_deref();
5653 let requested_architecture = query.installer_architecture.as_deref();
5654 let requested_type = query.installer_type.as_deref();
5655 let requested_scope = query.install_scope.as_deref();
5656 let requested_platform = query.platform.as_deref();
5657 let requested_os_version = query.os_version.as_deref();
5658 let system_architecture = current_architecture();
5659 let system_platform = current_platform();
5660 let current_os_version = current_os_version();
5661
5662 installers
5663 .iter()
5664 .enumerate()
5665 .filter(|(_, installer)| {
5666 installer_matches_requested(
5667 installer,
5668 requested_type,
5669 requested_scope,
5670 requested_platform,
5671 requested_os_version,
5672 system_platform,
5673 current_os_version.as_deref(),
5674 )
5675 })
5676 .filter(|(_, installer)| installer_matches_architecture(installer, requested_architecture, system_architecture))
5677 .max_by_key(|(index, installer)| {
5678 (
5679 installer_rank(installer, requested_locale, requested_architecture, system_architecture),
5680 std::cmp::Reverse(*index),
5681 )
5682 })
5683 .map(|(_, installer)| installer)
5684 .cloned()
5685}
5686
5687fn installer_matches_requested(
5688 installer: &Installer,
5689 requested_type: Option<&str>,
5690 requested_scope: Option<&str>,
5691 requested_platform: Option<&str>,
5692 requested_os_version: Option<&str>,
5693 system_platform: Option<&str>,
5694 current_os_version: Option<&str>,
5695) -> bool {
5696 matches_optional_ci(installer.installer_type.as_deref(), requested_type)
5697 && matches_optional_ci(installer.scope.as_deref(), requested_scope)
5698 && installer_matches_platform(installer, requested_platform, system_platform)
5699 && installer_matches_os_version(installer, requested_os_version, current_os_version)
5700}
5701
5702fn installer_matches_platform(
5703 installer: &Installer,
5704 requested_platform: Option<&str>,
5705 system_platform: Option<&str>,
5706) -> bool {
5707 if installer.platforms.is_empty() {
5708 return true;
5709 }
5710
5711 let Some(effective_platform) = requested_platform.or(system_platform) else {
5712 return true;
5713 };
5714
5715 installer
5716 .platforms
5717 .iter()
5718 .any(|platform| platform.eq_ignore_ascii_case(effective_platform))
5719}
5720
5721fn installer_matches_os_version(
5722 installer: &Installer,
5723 requested_os_version: Option<&str>,
5724 current_os_version: Option<&str>,
5725) -> bool {
5726 let Some(minimum_os_version) = installer.minimum_os_version.as_deref() else {
5727 return true;
5728 };
5729
5730 let Some(actual_os_version) = requested_os_version.or(current_os_version) else {
5731 return true;
5732 };
5733
5734 match (
5735 parse_dotted_version(minimum_os_version),
5736 parse_dotted_version(actual_os_version),
5737 ) {
5738 (Some(minimum), Some(actual)) => actual.cmp(&minimum) != Ordering::Less,
5739 _ => minimum_os_version.eq_ignore_ascii_case(actual_os_version),
5740 }
5741}
5742
5743fn installer_matches_architecture(
5744 installer: &Installer,
5745 requested_architecture: Option<&str>,
5746 system_architecture: &str,
5747) -> bool {
5748 let Some(architecture) = installer.architecture.as_deref() else {
5749 return true;
5750 };
5751
5752 let architecture = architecture.to_ascii_lowercase();
5753 if let Some(requested) = requested_architecture {
5754 return architecture.eq_ignore_ascii_case(requested);
5755 }
5756
5757 preferred_architectures(system_architecture)
5758 .iter()
5759 .any(|candidate| architecture.eq_ignore_ascii_case(candidate))
5760}
5761
5762fn installer_rank(
5763 installer: &Installer,
5764 requested_locale: Option<&str>,
5765 requested_architecture: Option<&str>,
5766 system_architecture: &str,
5767) -> (i32, i32, i32) {
5768 let architecture_rank = architecture_rank(
5769 installer.architecture.as_deref(),
5770 requested_architecture.unwrap_or(system_architecture),
5771 requested_architecture.is_some(),
5772 system_architecture,
5773 );
5774 let locale_rank = locale_rank(installer.locale.as_deref(), requested_locale);
5775 let command_rank = if installer.commands.is_empty() { 0 } else { 1 };
5776 (architecture_rank, locale_rank, command_rank)
5777}
5778
5779fn architecture_rank(
5780 installer_architecture: Option<&str>,
5781 preferred_architecture: &str,
5782 strict: bool,
5783 system_architecture: &str,
5784) -> i32 {
5785 let Some(value) = installer_architecture else {
5786 return 0;
5787 };
5788 if value.eq_ignore_ascii_case(preferred_architecture) {
5789 return 5;
5790 }
5791 if value.eq_ignore_ascii_case("neutral") {
5792 return 4;
5793 }
5794 if strict {
5795 return -1;
5796 }
5797
5798 preferred_architectures(system_architecture)
5799 .iter()
5800 .rev()
5801 .position(|candidate| value.eq_ignore_ascii_case(candidate))
5802 .and_then(|index| i32::try_from(index).ok())
5803 .and_then(|index| index.checked_add(1))
5804 .unwrap_or(-1)
5805}
5806
5807fn preferred_architectures(system_architecture: &str) -> &'static [&'static str] {
5808 match system_architecture {
5809 "arm64" => &["arm64", "neutral", "x64", "x86"],
5810 "x64" => &["x64", "neutral", "x86"],
5811 "x86" => &["x86", "neutral"],
5812 _ => &["neutral"],
5813 }
5814}
5815
5816fn locale_rank(installer_locale: Option<&str>, requested_locale: Option<&str>) -> i32 {
5817 match (installer_locale, requested_locale) {
5818 (Some(installer), Some(requested)) if installer.eq_ignore_ascii_case(requested) => 3,
5819 (Some(installer), Some(requested))
5820 if installer
5821 .split('-')
5822 .next()
5823 .zip(requested.split('-').next())
5824 .is_some_and(|(left, right)| left.eq_ignore_ascii_case(right)) =>
5825 {
5826 2
5827 }
5828 (None, Some(_)) => 1,
5829 (Some(_), None) => 1,
5830 (None, None) => 0,
5831 _ => -1,
5832 }
5833}
5834
5835fn matches_optional_ci(value: Option<&str>, requested: Option<&str>) -> bool {
5836 match requested {
5837 Some(requested) => value.is_some_and(|value| value.eq_ignore_ascii_case(requested)),
5838 None => true,
5839 }
5840}
5841
5842fn current_architecture() -> &'static str {
5843 match std::env::consts::ARCH {
5844 "x86_64" => "x64",
5845 "x86" => "x86",
5846 "aarch64" => "arm64",
5847 _ => "neutral",
5848 }
5849}
5850
5851fn current_platform() -> Option<&'static str> {
5852 if cfg!(windows) { Some("Windows.Desktop") } else { None }
5853}
5854
5855#[cfg(windows)]
5856fn current_os_version() -> Option<String> {
5857 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
5858 let key = hklm.open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion").ok()?;
5859 let build: String = key.get_value("CurrentBuildNumber").ok()?;
5860 let ubr: u32 = key.get_value("UBR").unwrap_or(0);
5861 let major: u32 = key.get_value("CurrentMajorVersionNumber").unwrap_or(10);
5862 let minor: u32 = key.get_value("CurrentMinorVersionNumber").unwrap_or(0);
5863 Some(format!("{major}.{minor}.{build}.{ubr}"))
5864}
5865
5866#[cfg(not(windows))]
5867fn current_os_version() -> Option<String> {
5868 None
5869}
5870
5871fn parse_dotted_version(version: &str) -> Option<Vec<u64>> {
5872 let mut parsed = Vec::new();
5873 for component in version.split('.') {
5874 if component.is_empty() {
5875 continue;
5876 }
5877 parsed.push(component.parse().ok()?);
5878 }
5879
5880 if parsed.is_empty() { None } else { Some(parsed) }
5881}
5882
5883fn select_v1_version(
5884 versions: &[V1VersionRow],
5885 requested_version: Option<&str>,
5886 requested_channel: Option<&str>,
5887) -> Result<V1VersionRow> {
5888 let mut ordered = versions.to_vec();
5889 ordered.sort_by(|left, right| {
5890 compare_version_and_channel(&right.version, &right.channel, &left.version, &left.channel)
5891 });
5892
5893 if let Some(version) = requested_version {
5894 let channel = requested_channel.unwrap_or_default();
5895 return ordered
5896 .iter()
5897 .find(|candidate| {
5898 candidate.version.eq_ignore_ascii_case(version)
5899 && (channel.is_empty() || candidate.channel.eq_ignore_ascii_case(channel))
5900 })
5901 .cloned()
5902 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5903 }
5904
5905 ordered
5906 .into_iter()
5907 .next()
5908 .ok_or_else(|| anyhow!("package had no versions"))
5909}
5910
5911fn select_v2_version(
5912 versions: &[PackageVersionDataEntry],
5913 requested_version: Option<&str>,
5914) -> Result<PackageVersionDataEntry> {
5915 let mut ordered = versions.to_vec();
5916 ordered.sort_by(|left, right| compare_version(&right.version, &left.version));
5917
5918 if let Some(version) = requested_version {
5919 return ordered
5920 .iter()
5921 .find(|candidate| candidate.version.eq_ignore_ascii_case(version))
5922 .cloned()
5923 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5924 }
5925
5926 ordered
5927 .into_iter()
5928 .next()
5929 .ok_or_else(|| anyhow!("package had no V2 versions"))
5930}
5931
5932fn select_rest_version<'a>(
5933 versions: &'a [VersionKey],
5934 requested_version: Option<&str>,
5935 requested_channel: Option<&str>,
5936) -> Result<&'a VersionKey> {
5937 if let Some(version) = requested_version {
5938 let channel = requested_channel.unwrap_or_default();
5939 return versions
5940 .iter()
5941 .find(|candidate| {
5942 candidate.version.eq_ignore_ascii_case(version)
5943 && (channel.is_empty() || candidate.channel.eq_ignore_ascii_case(channel))
5944 })
5945 .ok_or_else(|| anyhow!("requested version {version} was not found"));
5946 }
5947
5948 versions.first().ok_or_else(|| anyhow!("package had no REST versions"))
5949}
5950
5951fn sort_versions_desc(versions: &mut [VersionKey]) {
5952 versions.sort_by(|left, right| {
5953 compare_version_and_channel(&right.version, &right.channel, &left.version, &left.channel)
5954 });
5955}
5956
5957fn compare_version_and_channel(
5958 left_version: &str,
5959 left_channel: &str,
5960 right_version: &str,
5961 right_channel: &str,
5962) -> Ordering {
5963 compare_version(left_version, right_version).then_with(|| {
5964 left_channel
5965 .to_ascii_lowercase()
5966 .cmp(&right_channel.to_ascii_lowercase())
5967 })
5968}
5969
5970fn compare_version(left: &str, right: &str) -> Ordering {
5971 let left_parts = tokenize_version(left);
5972 let right_parts = tokenize_version(right);
5973 let max_len = left_parts.len().max(right_parts.len());
5974
5975 for index in 0..max_len {
5976 let left_part = left_parts.get(index).map(String::as_str).unwrap_or("0");
5977 let right_part = right_parts.get(index).map(String::as_str).unwrap_or("0");
5978 let numeric = left_part.parse::<u64>().ok().zip(right_part.parse::<u64>().ok());
5979
5980 let ordering = if let Some((left_number, right_number)) = numeric {
5981 left_number.cmp(&right_number)
5982 } else {
5983 left_part.to_ascii_lowercase().cmp(&right_part.to_ascii_lowercase())
5984 };
5985
5986 if ordering != Ordering::Equal {
5987 return ordering;
5988 }
5989 }
5990
5991 Ordering::Equal
5992}
5993
5994fn tokenize_version(value: &str) -> Vec<String> {
5995 let mut parts = Vec::new();
5996 let mut current = String::new();
5997 let mut current_is_digit = None;
5998
5999 for ch in value.chars() {
6000 if ch == '.' || ch == '-' || ch == '_' || ch == '+' {
6001 if !current.is_empty() {
6002 parts.push(std::mem::take(&mut current));
6003 }
6004 current_is_digit = None;
6005 continue;
6006 }
6007
6008 let is_digit = ch.is_ascii_digit();
6009 match current_is_digit {
6010 Some(previous) if previous != is_digit => {
6011 parts.push(std::mem::take(&mut current));
6012 current.push(ch);
6013 current_is_digit = Some(is_digit);
6014 }
6015 Some(_) => current.push(ch),
6016 None => {
6017 current.push(ch);
6018 current_is_digit = Some(is_digit);
6019 }
6020 }
6021 }
6022
6023 if !current.is_empty() {
6024 parts.push(current);
6025 }
6026
6027 parts
6028}
6029
6030fn verify_hash(expected_hash: &str, bytes: &[u8]) -> Result<()> {
6031 if !hash_matches(Some(expected_hash), bytes) {
6032 bail!("downloaded file hash did not match expected SHA256");
6033 }
6034 Ok(())
6035}
6036
6037fn verify_installer_hash(expected_hash: Option<&str>, bytes: &[u8], ignore_security_hash: bool) -> Result<()> {
6038 let Some(expected_hash) = expected_hash else {
6039 return Ok(());
6040 };
6041 if ignore_security_hash {
6042 return Ok(());
6043 }
6044
6045 if !hash_matches(Some(expected_hash), bytes) {
6046 let actual = sha256_hex(bytes);
6047 bail!("Installer hash mismatch. Expected: {expected_hash}, Got: {actual}");
6048 }
6049 Ok(())
6050}
6051
6052fn hash_matches(expected_hash: Option<&str>, bytes: &[u8]) -> bool {
6053 expected_hash
6054 .map(|expected| sha256_hex(bytes).eq_ignore_ascii_case(expected))
6055 .unwrap_or(true)
6056}
6057
6058fn sha256_hex(bytes: &[u8]) -> String {
6059 let digest = Sha256::digest(bytes);
6060 let mut output = String::with_capacity(digest.len() * 2);
6061 for byte in digest {
6062 output.push_str(&format!("{byte:02X}"));
6063 }
6064 output
6065}
6066
6067fn pins_db_path(app_root: &Path) -> PathBuf {
6068 if uses_packaged_layout(app_root) {
6069 app_root.join("pinning.db")
6070 } else {
6071 app_root.join("pins.db")
6072 }
6073}
6074
6075fn resolve_pin_type_column(conn: &Connection) -> Result<Option<&'static str>> {
6076 let mut stmt = conn.prepare("PRAGMA table_info(pin)")?;
6077 let mut has_current_column = false;
6078 let mut has_legacy_column = false;
6079
6080 let column_names = stmt.query_map([], |row| row.get::<_, String>(1))?;
6081 for column_name in column_names {
6082 match column_name?.as_str() {
6083 "type" => has_current_column = true,
6084 "pin_type" => has_legacy_column = true,
6085 _ => {}
6086 }
6087 }
6088
6089 Ok(if has_current_column {
6090 Some("type")
6091 } else if has_legacy_column {
6092 Some("pin_type")
6093 } else {
6094 None
6095 })
6096}
6097
6098fn decode_pin_type(pin_type_int: i64) -> PinType {
6099 match pin_type_int {
6100 4 => PinType::Blocking,
6101 3 => PinType::Gating,
6102 _ => PinType::Pinning,
6103 }
6104}
6105
6106#[cfg(windows)]
6107fn dispatch_installer(
6108 installer_path: &Path,
6109 installer_type: &str,
6110 request: &InstallRequest,
6111 manifest: &Manifest,
6112 installer: &Installer,
6113) -> Result<i32> {
6114 use std::process::Command;
6115
6116 match installer_type {
6117 "msi" | "wix" => {
6118 let mut cmd = Command::new("msiexec");
6119 cmd.args(installer_command_arguments(
6120 installer_type,
6121 request,
6122 manifest,
6123 installer_path,
6124 installer,
6125 ));
6126 let status = cmd.status().context("failed to run msiexec")?;
6127 Ok(status.code().unwrap_or(-1))
6128 }
6129 "msix" | "appx" => {
6130 let mut cmd = Command::new("powershell");
6131 cmd.arg("-NoProfile")
6132 .arg("-Command")
6133 .arg(format!("Add-AppxPackage -Path '{}'", installer_path.display()));
6134 let status = cmd.status().context("failed to run Add-AppxPackage")?;
6135 Ok(status.code().unwrap_or(-1))
6136 }
6137 "zip" => {
6138 let target = dirs::data_local_dir()
6139 .unwrap_or_else(|| PathBuf::from("."))
6140 .join("Programs");
6141 fs::create_dir_all(&target)?;
6142 let file = fs::File::open(installer_path)?;
6143 let mut archive = ZipArchive::new(file)?;
6144 archive.extract(&target)?;
6145 Ok(0)
6146 }
6147 _ => {
6149 let mut cmd = Command::new(installer_path);
6150 cmd.args(installer_command_arguments(
6151 installer_type,
6152 request,
6153 manifest,
6154 installer_path,
6155 installer,
6156 ));
6157 let status = cmd.status().context("failed to run installer")?;
6158 Ok(status.code().unwrap_or(-1))
6159 }
6160 }
6161}
6162
6163#[cfg(not(windows))]
6164fn dispatch_installer(
6165 _installer_path: &Path,
6166 _installer_type: &str,
6167 _request: &InstallRequest,
6168 _manifest: &Manifest,
6169 _installer: &Installer,
6170) -> Result<i32> {
6171 bail!("Installing packages is only supported on Windows")
6172}
6173
6174#[cfg(any(windows, test))]
6175fn installer_command_arguments(
6176 installer_type: &str,
6177 request: &InstallRequest,
6178 _manifest: &Manifest,
6179 installer_path: &Path,
6180 installer: &Installer,
6181) -> Vec<String> {
6182 if let Some(override_args) = request.override_args.as_deref() {
6183 return split_installer_switches(override_args);
6184 }
6185
6186 let mut args = Vec::new();
6187 match installer_type {
6188 "msi" | "wix" => {
6189 args.push("/i".to_owned());
6190 args.push(installer_path.display().to_string());
6191 }
6192 "msix" | "appx" | "zip" => return Vec::new(),
6193 _ => {}
6194 }
6195
6196 let configured = match request.mode {
6197 InstallerMode::Interactive => installer.switches.interactive.as_deref(),
6198 InstallerMode::SilentWithProgress => installer
6199 .switches
6200 .silent_with_progress
6201 .as_deref()
6202 .or(installer.switches.silent.as_deref()),
6203 InstallerMode::Silent => installer
6204 .switches
6205 .silent
6206 .as_deref()
6207 .or(installer.switches.silent_with_progress.as_deref()),
6208 };
6209
6210 if let Some(value) = configured.filter(|value| !value.trim().is_empty()) {
6211 args.extend(split_installer_switches(value));
6212 } else {
6213 args.extend(default_installer_arguments(installer_type, request.mode));
6214 }
6215
6216 append_switches(
6217 &mut args,
6218 resolve_template_switch(
6219 installer.switches.log.as_deref(),
6220 default_log_switch(installer_type),
6221 request.log_path.as_ref().map(|path| path.display().to_string()),
6222 "<LOGPATH>",
6223 ),
6224 );
6225 append_switches(&mut args, installer.switches.custom.clone());
6226 append_switches(&mut args, request.custom.clone());
6227 append_switches(
6228 &mut args,
6229 resolve_template_switch(
6230 installer.switches.install_location.as_deref(),
6231 default_install_location_switch(installer_type),
6232 request.install_location.clone(),
6233 "<INSTALLPATH>",
6234 ),
6235 );
6236 args
6237}
6238
6239#[cfg(any(windows, test))]
6240fn default_installer_arguments(installer_type: &str, mode: InstallerMode) -> Vec<String> {
6241 match mode {
6242 InstallerMode::Interactive => Vec::new(),
6243 InstallerMode::SilentWithProgress => match installer_type {
6244 "inno" => vec![
6245 "/SP-".to_owned(),
6246 "/SILENT".to_owned(),
6247 "/SUPPRESSMSGBOXES".to_owned(),
6248 "/NORESTART".to_owned(),
6249 ],
6250 "burn" | "wix" | "msi" => vec!["/passive".to_owned(), "/norestart".to_owned()],
6251 "nullsoft" | "nsis" => vec!["/S".to_owned()],
6252 _ => vec!["/SILENT".to_owned()],
6253 },
6254 InstallerMode::Silent => match installer_type {
6255 "inno" => vec![
6256 "/SP-".to_owned(),
6257 "/VERYSILENT".to_owned(),
6258 "/SUPPRESSMSGBOXES".to_owned(),
6259 "/NORESTART".to_owned(),
6260 ],
6261 "burn" | "wix" | "msi" => vec!["/quiet".to_owned(), "/norestart".to_owned()],
6262 "nullsoft" | "nsis" => vec!["/S".to_owned()],
6263 _ => vec!["/S".to_owned()],
6264 },
6265 }
6266}
6267
6268#[cfg(any(windows, test))]
6269fn default_log_switch(installer_type: &str) -> Option<&'static str> {
6270 match installer_type {
6271 "burn" | "wix" | "msi" => Some("/log \"<LOGPATH>\""),
6272 "inno" => Some("/LOG=\"<LOGPATH>\""),
6273 _ => None,
6274 }
6275}
6276
6277#[cfg(any(windows, test))]
6278fn default_install_location_switch(installer_type: &str) -> Option<&'static str> {
6279 match installer_type {
6280 "burn" | "wix" | "msi" => Some("TARGETDIR=\"<INSTALLPATH>\""),
6281 "nullsoft" | "nsis" => Some("/D=<INSTALLPATH>"),
6282 "inno" => Some("/DIR=\"<INSTALLPATH>\""),
6283 _ => None,
6284 }
6285}
6286
6287#[cfg(any(windows, test))]
6288fn resolve_template_switch(
6289 manifest_value: Option<&str>,
6290 fallback: Option<&str>,
6291 replacement: Option<String>,
6292 token: &str,
6293) -> Option<String> {
6294 let template = manifest_value.filter(|value| !value.trim().is_empty()).or(fallback)?;
6295 let replacement = replacement?;
6296 Some(template.replace(token, &replacement))
6297}
6298
6299#[cfg(any(windows, test))]
6300fn append_switches(args: &mut Vec<String>, value: Option<String>) {
6301 if let Some(value) = value.filter(|value| !value.trim().is_empty()) {
6302 args.extend(split_installer_switches(&value));
6303 }
6304}
6305
6306#[cfg(any(windows, test))]
6307fn split_installer_switches(value: &str) -> Vec<String> {
6308 let mut args = Vec::new();
6309 let mut current = String::new();
6310 let mut in_quotes = false;
6311
6312 for ch in value.chars() {
6313 match ch {
6314 '"' => in_quotes = !in_quotes,
6315 c if c.is_whitespace() && !in_quotes => {
6316 if !current.is_empty() {
6317 args.push(std::mem::take(&mut current));
6318 }
6319 }
6320 _ => current.push(ch),
6321 }
6322 }
6323
6324 if !current.is_empty() {
6325 args.push(current);
6326 }
6327
6328 args
6329}
6330
6331#[cfg(windows)]
6332fn uninstall_package(installed: &ListMatch, request: &UninstallRequest) -> Result<i32> {
6333 if request.purge && request.preserve {
6334 bail!("--purge and --preserve cannot be used together.");
6335 }
6336
6337 if installed
6338 .installer_category
6339 .as_deref()
6340 .is_some_and(|value| value.eq_ignore_ascii_case("portable"))
6341 {
6342 return uninstall_portable(installed, request);
6343 }
6344
6345 if (request.purge || request.preserve) && !request.force {
6346 bail!("--purge and --preserve are currently only supported for portable packages.");
6347 }
6348
6349 if let Some(exit_code) = try_uninstall_arp(installed, request)? {
6350 return Ok(exit_code);
6351 }
6352
6353 if let Some(exit_code) = try_uninstall_msix(installed)? {
6354 return Ok(exit_code);
6355 }
6356
6357 bail!(
6358 "No uninstall command found for installed package '{}' ({})",
6359 installed.name,
6360 installed.local_id
6361 );
6362}
6363
6364#[cfg(windows)]
6365fn try_uninstall_arp(installed: &ListMatch, request: &UninstallRequest) -> Result<Option<i32>> {
6366 use std::os::windows::process::CommandExt;
6367 use std::process::Command;
6368
6369 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
6370 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
6371
6372 let arp_paths = [
6373 r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
6374 r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
6375 ];
6376
6377 for root in [&hklm, &hkcu] {
6378 for arp_path in &arp_paths {
6379 if let Ok(key) = root.open_subkey(arp_path) {
6380 for subkey_name in key.enum_keys().filter_map(|k| k.ok()) {
6381 if let Ok(subkey) = key.open_subkey(&subkey_name) {
6382 let display_name: String = subkey.get_value("DisplayName").unwrap_or_default();
6383 let product_code = subkey.get_value::<String, _>("ProductCode").ok();
6384 if !registry_entry_matches_installed_package(
6385 &subkey_name,
6386 &display_name,
6387 product_code.as_deref(),
6388 installed,
6389 ) {
6390 continue;
6391 }
6392
6393 if let Some(exit_code) =
6394 try_run_msi_uninstall(installed, &subkey_name, product_code.as_deref(), request)?
6395 {
6396 return Ok(Some(exit_code));
6397 }
6398
6399 let quiet_uninstall_cmd = subkey.get_value::<String, _>("QuietUninstallString").ok();
6400 let uninstall_cmd: String = if request.mode == InstallerMode::Interactive {
6401 subkey.get_value("UninstallString").ok()
6402 } else {
6403 quiet_uninstall_cmd
6404 .clone()
6405 .or_else(|| subkey.get_value("UninstallString").ok())
6406 }
6407 .context("No uninstall command found in registry")?;
6408
6409 if is_winget_uninstall_command(&uninstall_cmd)
6410 && let Some(exit_code) = try_uninstall_via_winget(installed, request)?
6411 {
6412 return Ok(Some(exit_code));
6413 }
6414
6415 let log_path = request.log_path.as_ref().map(|value| value.display().to_string());
6416 let uninstall_line = build_uninstall_command_with_mode(
6417 &uninstall_cmd,
6418 request.mode,
6419 quiet_uninstall_cmd.is_some(),
6420 log_path.as_deref(),
6421 );
6422 let mut cmd = Command::new("cmd");
6431 cmd.raw_arg("/S /C");
6432 cmd.raw_arg(format!("\"{uninstall_line}\""));
6433 let status = cmd.status().context("failed to run uninstaller")?;
6434 return Ok(Some(status.code().unwrap_or(-1)));
6435 }
6436 }
6437 }
6438 }
6439 }
6440
6441 Ok(None)
6442}
6443
6444#[cfg(windows)]
6445fn try_uninstall_msix(installed: &ListMatch) -> Result<Option<i32>> {
6446 use std::process::Command;
6447
6448 if !installed.local_id.starts_with("MSIX\\") && installed.package_family_names.is_empty() {
6449 return Ok(None);
6450 }
6451
6452 let package_full_name = installed.local_id.strip_prefix("MSIX\\");
6453 let mut cmd = Command::new("powershell");
6454 cmd.arg("-NoProfile").arg("-Command").arg(build_msix_uninstall_script(
6455 package_full_name,
6456 &installed.package_family_names,
6457 ));
6458 let status = cmd.status().context("failed to run Remove-AppxPackage")?;
6459 Ok(Some(status.code().unwrap_or(-1)))
6460}
6461
6462#[cfg(windows)]
6463fn arp_subkey_name(local_id: &str) -> Option<&str> {
6464 local_id.strip_prefix("ARP\\")?.splitn(3, '\\').nth(2)
6465}
6466
6467#[cfg(windows)]
6468fn registry_entry_matches_installed_package(
6469 subkey_name: &str,
6470 display_name: &str,
6471 product_code: Option<&str>,
6472 installed: &ListMatch,
6473) -> bool {
6474 if let Some(arp_subkey_name) = arp_subkey_name(&installed.local_id)
6475 && subkey_name.eq_ignore_ascii_case(arp_subkey_name)
6476 {
6477 return true;
6478 }
6479
6480 if installed.product_codes.iter().any(|code| {
6481 code.eq_ignore_ascii_case(subkey_name)
6482 || product_code
6483 .map(|value| code.eq_ignore_ascii_case(value))
6484 .unwrap_or(false)
6485 }) {
6486 return true;
6487 }
6488
6489 display_name.eq_ignore_ascii_case(&installed.name)
6490}
6491
6492#[cfg(all(windows, test))]
6493fn build_uninstall_command(uninstall_cmd: &str, silent: bool, has_quiet_command: bool) -> String {
6494 build_uninstall_command_with_mode(
6495 uninstall_cmd,
6496 if silent {
6497 InstallerMode::Silent
6498 } else {
6499 InstallerMode::Interactive
6500 },
6501 has_quiet_command,
6502 None,
6503 )
6504}
6505
6506#[cfg(windows)]
6507fn build_uninstall_command_with_mode(
6508 uninstall_cmd: &str,
6509 mode: InstallerMode,
6510 has_quiet_command: bool,
6511 log_path: Option<&str>,
6512) -> String {
6513 let uninstall_cmd = log_path
6514 .map(|value| uninstall_cmd.replace("<LOGPATH>", value))
6515 .unwrap_or_else(|| uninstall_cmd.to_owned());
6516 if mode == InstallerMode::Interactive || has_quiet_command {
6517 return uninstall_cmd;
6518 }
6519
6520 let lower = uninstall_cmd.to_ascii_lowercase();
6521 if is_winget_uninstall_command_lower(&lower) {
6522 return uninstall_cmd;
6523 }
6524 if lower.contains("/quiet")
6525 || lower.contains("/passive")
6526 || lower.contains("/verysilent")
6527 || lower.contains("/silent")
6528 || lower.contains(" /s")
6529 {
6530 return uninstall_cmd;
6531 }
6532
6533 format!("{uninstall_cmd} /S")
6534}
6535
6536#[cfg(windows)]
6537fn try_uninstall_via_winget(installed: &ListMatch, request: &UninstallRequest) -> Result<Option<i32>> {
6538 let Some(args) = build_winget_uninstall_arguments(installed, request) else {
6539 return Ok(None);
6540 };
6541
6542 let exit_code = run_winget_uninstall(&args)?;
6543 if exit_code == WINGET_PACKAGE_NOT_FOUND_EXIT_CODE && request.query.install_scope.is_some() {
6544 let Some(args_without_scope) = build_winget_uninstall_arguments_with_scope(installed, request, false) else {
6545 return Ok(Some(exit_code));
6546 };
6547
6548 return Ok(Some(run_winget_uninstall(&args_without_scope)?));
6549 }
6550
6551 Ok(Some(exit_code))
6552}
6553
6554#[cfg(windows)]
6555fn run_winget_uninstall(args: &[String]) -> Result<i32> {
6556 use std::process::Command;
6557
6558 let status = Command::new("winget")
6559 .args(args)
6560 .status()
6561 .context("failed to run delegated winget uninstall")?;
6562 Ok(status.code().unwrap_or(-1))
6563}
6564
6565#[cfg(windows)]
6566fn build_winget_uninstall_arguments(installed: &ListMatch, request: &UninstallRequest) -> Option<Vec<String>> {
6567 build_winget_uninstall_arguments_with_scope(installed, request, true)
6568}
6569
6570#[cfg(windows)]
6571fn build_winget_uninstall_arguments_with_scope(
6572 installed: &ListMatch,
6573 request: &UninstallRequest,
6574 include_scope: bool,
6575) -> Option<Vec<String>> {
6576 if installed.id.eq_ignore_ascii_case(&installed.local_id) {
6577 return None;
6578 }
6579
6580 let mut args = vec![
6581 "uninstall".to_owned(),
6582 "--id".to_owned(),
6583 installed.id.clone(),
6584 "--exact".to_owned(),
6585 "--disable-interactivity".to_owned(),
6586 ];
6587
6588 if let Some(source_name) = installed.source_name.as_deref().filter(|value| !value.is_empty()) {
6589 args.push("--source".to_owned());
6590 args.push(source_name.to_owned());
6591 }
6592
6593 if include_scope && let Some(scope) = request.query.install_scope.as_deref().filter(|value| !value.is_empty()) {
6594 args.push("--scope".to_owned());
6595 args.push(scope.to_owned());
6596 }
6597
6598 if request.mode == InstallerMode::Silent {
6599 args.push("--silent".to_owned());
6600 }
6601
6602 Some(args)
6603}
6604
6605#[cfg(windows)]
6606fn is_winget_uninstall_command(command: &str) -> bool {
6607 is_winget_uninstall_command_lower(&command.to_ascii_lowercase())
6608}
6609
6610#[cfg(windows)]
6611fn is_winget_uninstall_command_lower(command: &str) -> bool {
6612 command.contains("winget uninstall") || command.contains("winget.exe uninstall")
6613}
6614
6615#[cfg(windows)]
6616fn build_msix_uninstall_script(package_full_name: Option<&str>, package_family_names: &[String]) -> String {
6617 let full_name_literal = package_full_name
6618 .map(|value| format!("'{}'", value.replace('\'', "''")))
6619 .unwrap_or_else(|| "$null".to_owned());
6620 let family_names_literal = if package_family_names.is_empty() {
6621 "@()".to_owned()
6622 } else {
6623 format!(
6624 "@({})",
6625 package_family_names
6626 .iter()
6627 .map(|value| format!("'{}'", value.replace('\'', "''")))
6628 .collect::<Vec<_>>()
6629 .join(", ")
6630 )
6631 };
6632
6633 format!(
6634 "$fullName = {full_name_literal}; \
6635 $familyNames = {family_names_literal}; \
6636 $targets = Get-AppxPackage | Where-Object {{ \
6637 (($fullName -ne $null) -and $_.PackageFullName -eq $fullName) -or \
6638 ($familyNames.Count -gt 0 -and ($familyNames -contains $_.PackageFamilyName)) }}; \
6639 if (-not $targets) {{ exit 1 }}; \
6640 $targets | Remove-AppxPackage"
6641 )
6642}
6643
6644#[cfg(windows)]
6645fn try_run_msi_uninstall(
6646 installed: &ListMatch,
6647 subkey_name: &str,
6648 product_code: Option<&str>,
6649 request: &UninstallRequest,
6650) -> Result<Option<i32>> {
6651 use std::process::Command;
6652
6653 let uninstall_code = installed
6654 .product_codes
6655 .iter()
6656 .map(String::as_str)
6657 .chain(product_code)
6658 .chain(Some(subkey_name))
6659 .find(|value| is_product_code_like(value));
6660
6661 let Some(uninstall_code) = uninstall_code else {
6662 return Ok(None);
6663 };
6664
6665 let mut cmd = Command::new("msiexec");
6666 cmd.arg("/x").arg(uninstall_code);
6667 match request.mode {
6668 InstallerMode::Interactive => {}
6669 InstallerMode::SilentWithProgress => {
6670 cmd.arg("/passive").arg("/norestart");
6671 }
6672 InstallerMode::Silent => {
6673 cmd.arg("/quiet").arg("/norestart");
6674 }
6675 }
6676 if let Some(log_path) = &request.log_path {
6677 cmd.arg("/log").arg(log_path);
6678 }
6679
6680 let status = cmd.status().context("failed to run msiexec uninstall")?;
6681 Ok(Some(status.code().unwrap_or(-1)))
6682}
6683
6684#[cfg(windows)]
6685fn uninstall_portable(installed: &ListMatch, request: &UninstallRequest) -> Result<i32> {
6686 let Some(location) = installed.install_location.as_deref() else {
6687 if request.force {
6688 return Ok(0);
6689 }
6690 bail!(
6691 "Portable package '{}' does not expose an install location.",
6692 installed.name
6693 );
6694 };
6695
6696 if request.preserve {
6697 return Ok(0);
6698 }
6699
6700 let path = Path::new(location);
6701 if path.is_dir() {
6702 fs::remove_dir_all(path)?;
6703 return Ok(0);
6704 }
6705 if path.is_file() {
6706 fs::remove_file(path)?;
6707 return Ok(0);
6708 }
6709 if request.force {
6710 return Ok(0);
6711 }
6712
6713 bail!("Portable package location not found: {location}")
6714}
6715
6716#[cfg(windows)]
6717fn is_product_code_like(value: &str) -> bool {
6718 value.starts_with('{') && value.ends_with('}')
6719}
6720
6721#[cfg(not(windows))]
6722fn uninstall_package(_installed: &ListMatch, _request: &UninstallRequest) -> Result<i32> {
6723 bail!("Uninstalling packages is only supported on Windows")
6724}
6725
6726#[cfg(test)]
6727mod tests {
6728 use std::io::Write;
6729 use std::path::{Path, PathBuf};
6730
6731 use flate2::Compression;
6732 use flate2::write::DeflateEncoder;
6733
6734 use super::*;
6735
6736 fn temp_app_root(label: &str) -> PathBuf {
6737 std::env::temp_dir().join(format!(
6738 "pinget-rs-tests-{label}-{}",
6739 Utc::now().timestamp_nanos_opt().unwrap_or_default()
6740 ))
6741 }
6742
6743 #[test]
6744 fn repository_options_capture_custom_host_settings() {
6745 let options =
6746 RepositoryOptions::new(PathBuf::from(r"C:\temp\pinget-test")).with_user_agent("pinget-rs-tests/1.0");
6747
6748 assert_eq!(options.app_root, PathBuf::from(r"C:\temp\pinget-test"));
6749 assert_eq!(options.user_agent, "pinget-rs-tests/1.0");
6750 }
6751
6752 #[test]
6753 fn dependency_install_requests_are_silent_and_not_forced() {
6754 let mut parent = InstallRequest::new(PackageQuery {
6755 source: Some("winget".to_owned()),
6756 ..PackageQuery::default()
6757 });
6758 parent.mode = InstallerMode::SilentWithProgress;
6759 parent.force = true;
6760 parent.accept_package_agreements = true;
6761 parent.ignore_security_hash = true;
6762 parent.dependency_source = Some("dependencies".to_owned());
6763
6764 let dependency =
6765 Repository::create_dependency_install_request("Microsoft.VCRedist.2015+.x64".to_owned(), &parent);
6766
6767 assert_eq!(dependency.query.id.as_deref(), Some("Microsoft.VCRedist.2015+.x64"));
6768 assert_eq!(dependency.query.source.as_deref(), Some("dependencies"));
6769 assert_eq!(dependency.mode, InstallerMode::Silent);
6770 assert!(!dependency.force);
6771 assert!(dependency.no_upgrade);
6772 assert!(dependency.accept_package_agreements);
6773 assert!(dependency.ignore_security_hash);
6774 }
6775
6776 #[test]
6777 fn storage_paths_use_configured_app_root() {
6778 let app_root = PathBuf::from(r"C:\temp\pinget-test");
6779 let source = SourceRecord {
6780 name: "winget/test".to_owned(),
6781 kind: SourceKind::PreIndexed,
6782 arg: "https://example.com/cache".to_owned(),
6783 identifier: "Test.Source".to_owned(),
6784 trust_level: "None".to_owned(),
6785 explicit: false,
6786 priority: 0,
6787 last_update: None,
6788 source_version: None,
6789 };
6790
6791 assert_eq!(store_path(&app_root), app_root.join("sources.json"));
6792 assert_eq!(
6793 source_state_dir(&app_root, &source),
6794 app_root.join("sources").join("winget_test")
6795 );
6796 assert_eq!(pins_db_path(&app_root), app_root.join("pins.db"));
6797 }
6798
6799 #[cfg(windows)]
6800 #[test]
6801 fn packaged_layout_paths_resolve_from_packaged_app_root() {
6802 let local_app_data = dirs::data_local_dir().expect("LocalAppData");
6803 let app_root = local_app_data
6804 .join("Packages")
6805 .join(PACKAGED_FAMILY_NAME)
6806 .join("LocalState");
6807 assert!(uses_packaged_layout(&app_root));
6808 assert!(
6809 user_settings_path(&app_root)
6810 .display()
6811 .to_string()
6812 .ends_with(&format!(r"Packages\{}\LocalState\settings.json", PACKAGED_FAMILY_NAME))
6813 );
6814 assert!(
6815 packaged_file_cache_root(&app_root)
6816 .display()
6817 .to_string()
6818 .ends_with(&format!(
6819 r"Packages\{}\LocalState\Microsoft\Windows Package Manager",
6820 PACKAGED_FAMILY_NAME
6821 ))
6822 );
6823 }
6824
6825 #[cfg(windows)]
6826 #[test]
6827 fn default_app_root_outside_package_avoids_packaged_layout() {
6828 let prior = std::env::var_os("PINGET_APPROOT");
6832 unsafe { std::env::remove_var("PINGET_APPROOT") };
6834
6835 let result = default_app_root();
6836
6837 if let Some(prior) = prior {
6838 unsafe { std::env::set_var("PINGET_APPROOT", prior) };
6840 }
6841
6842 let app_root = result.expect("default app root");
6843 assert!(!uses_packaged_layout(&app_root), "got packaged layout for {app_root:?}");
6844 assert!(
6845 app_root.ends_with(Path::new("Devolutions").join("Pinget")),
6846 "unexpected fallback app root: {app_root:?}"
6847 );
6848 }
6849
6850 #[test]
6851 fn packaged_source_stream_overlays_defaults_and_metadata() {
6852 let store = parse_packaged_source_store(
6853 Some(
6854 "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",
6855 ),
6856 Some(
6857 "Sources:\n - Name: corp\n LastUpdate: 1700000000\n SourceVersion: 1.2.3\n",
6858 ),
6859 )
6860 .expect("packaged store");
6861
6862 assert!(!store.sources.iter().any(|source| source.name == "winget"));
6863 let corp = store
6864 .sources
6865 .iter()
6866 .find(|source| source.name == "corp")
6867 .expect("corp source");
6868 assert_eq!(corp.kind, SourceKind::Rest);
6869 assert_eq!(corp.identifier, "Contoso.Rest");
6870 assert_eq!(corp.trust_level, "Trusted");
6871 assert!(corp.explicit);
6872 assert_eq!(corp.priority, 7);
6873 assert_eq!(corp.source_version.as_deref(), Some("1.2.3"));
6874 assert_eq!(corp.last_update, DateTime::<Utc>::from_timestamp(1_700_000_000, 0));
6875 }
6876
6877 #[test]
6878 fn system_winget_export_parses_json_lines() {
6879 let output = r#"{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
6880{"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"}
6881"#;
6882
6883 let sources = parse_system_winget_source_export(output).expect("source export");
6884
6885 assert_eq!(
6886 sources.iter().map(|source| source.name.as_str()).collect::<Vec<_>>(),
6887 vec!["contoso", "contoso-cache"]
6888 );
6889 assert_eq!(sources[0].kind, SourceKind::Rest);
6890 assert_eq!(sources[0].arg, "https://api.contoso.test/feed");
6891 assert_eq!(sources[0].identifier, "api.contoso.test");
6892 assert_eq!(sources[0].trust_level, "Trusted");
6893 assert!(!sources[0].explicit);
6894 assert_eq!(sources[1].kind, SourceKind::PreIndexed);
6895 assert_eq!(sources[1].identifier, "Contoso.Source_8wekyb3d8bbwe");
6896 assert!(sources[1].explicit);
6897 }
6898
6899 #[test]
6900 fn system_winget_export_parses_wrapped_sources() {
6901 let output = r#"{
6902 "Sources": [
6903 {
6904 "Name": "corp",
6905 "Type": "Microsoft.Rest",
6906 "Arg": "https://packages.contoso.test/api",
6907 "Identifier": "Contoso.Rest",
6908 "TrustLevel": "Trusted",
6909 "Explicit": true,
6910 "Priority": 9
6911 }
6912 ]
6913}"#;
6914
6915 let sources = parse_system_winget_source_export(output).expect("source export");
6916 let source = sources.first().expect("source");
6917 assert_eq!(sources.len(), 1);
6918 assert_eq!(source.name, "corp");
6919 assert_eq!(source.kind, SourceKind::Rest);
6920 assert_eq!(source.identifier, "Contoso.Rest");
6921 assert_eq!(source.trust_level, "Trusted");
6922 assert!(source.explicit);
6923 assert_eq!(source.priority, 9);
6924 }
6925
6926 #[test]
6927 fn system_winget_source_store_detects_secure_settings_stub() {
6928 assert!(is_secure_settings_stub(Some(
6929 "SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334"
6930 )));
6931 assert!(!is_secure_settings_stub(Some("Sources:\n - Name: winget\n")));
6932 }
6933
6934 #[test]
6935 fn system_winget_source_store_builds_add_arguments() {
6936 let args = build_winget_source_add_arguments(
6937 "corp",
6938 "https://packages.contoso.test/api",
6939 SourceKind::Rest,
6940 "Trusted",
6941 true,
6942 );
6943
6944 assert_eq!(
6945 args,
6946 vec![
6947 "source",
6948 "add",
6949 "--name",
6950 "corp",
6951 "--arg",
6952 "https://packages.contoso.test/api",
6953 "--type",
6954 "Microsoft.Rest",
6955 "--disable-interactivity",
6956 "--trust-level",
6957 "trusted",
6958 "--explicit",
6959 ]
6960 );
6961 }
6962
6963 fn fake_system_winget_source_export(_args: &[String]) -> Result<WingetSourceCommandResult> {
6964 Ok(WingetSourceCommandResult {
6965 exit_code: 0,
6966 stdout: r#"{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"}
6967"#
6968 .to_owned(),
6969 stderr: String::new(),
6970 })
6971 }
6972
6973 #[test]
6974 fn packaged_secure_settings_stub_delegates_to_system_winget_export() {
6975 let original_runner = {
6976 let mut runner = SYSTEM_WINGET_SOURCE_COMMAND_RUNNER.write().expect("runner lock");
6977 let original = *runner;
6978 *runner = fake_system_winget_source_export;
6979 original
6980 };
6981
6982 let result = load_packaged_store_from_streams(
6983 Some("SHA256: d52f7aa273206e81585b714fc627ecb6f6e17b6aeba7b28025124da0d25db334"),
6984 None,
6985 );
6986
6987 {
6988 let mut runner = SYSTEM_WINGET_SOURCE_COMMAND_RUNNER.write().expect("runner lock");
6989 *runner = original_runner;
6990 }
6991
6992 let store = result.expect("packaged store").expect("packaged store");
6993 let source = store.sources.first().expect("source");
6994 assert_eq!(store.sources.len(), 1);
6995 assert_eq!(source.name, "contoso");
6996 assert_eq!(source.kind, SourceKind::Rest);
6997 }
6998
6999 #[test]
7000 fn source_metadata_and_named_reset_round_trip() {
7001 let app_root = temp_app_root("source-reset");
7002 let result = (|| -> Result<()> {
7003 let mut repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
7004 repository.add_source_with_metadata(
7005 "test",
7006 "https://example.com/test",
7007 SourceKind::Rest,
7008 Some("trusted"),
7009 true,
7010 4,
7011 )?;
7012
7013 repository.edit_source("test", Some(false), None)?;
7014 let edited = repository
7015 .list_sources()
7016 .into_iter()
7017 .find(|source| source.name == "test")
7018 .expect("source");
7019 assert_eq!(edited.trust_level, "Trusted");
7020 assert!(!edited.explicit);
7021 assert_eq!(edited.priority, 4);
7022
7023 repository.reset_source("test")?;
7024 let reset = repository
7025 .list_sources()
7026 .into_iter()
7027 .find(|source| source.name == "test")
7028 .expect("source");
7029 assert_eq!(reset.trust_level, "Trusted");
7030 assert!(!reset.explicit);
7031 assert_eq!(reset.priority, 4);
7032 assert!(reset.last_update.is_none());
7033 assert!(reset.source_version.is_none());
7034 Ok(())
7035 })();
7036
7037 let _ = fs::remove_dir_all(&app_root);
7038 result.expect("source metadata");
7039 }
7040
7041 #[test]
7042 fn explicit_sources_are_skipped_by_default_search_resolution() {
7043 let app_root = temp_app_root("source-resolution");
7044 let result = (|| -> Result<()> {
7045 let mut repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
7046 repository.add_source_with_metadata(
7047 "explicit-test",
7048 "https://example.com/explicit",
7049 SourceKind::Rest,
7050 Some("trusted"),
7051 true,
7052 10,
7053 )?;
7054
7055 let indexes = repository.resolve_source_indexes(None)?;
7056 let names: Vec<_> = indexes
7057 .into_iter()
7058 .map(|index| repository.list_sources()[index].name.clone())
7059 .collect();
7060 assert!(!names.iter().any(|name| name == "explicit-test"));
7061 Ok(())
7062 })();
7063
7064 let _ = fs::remove_dir_all(&app_root);
7065 result.expect("source resolution");
7066 }
7067
7068 #[test]
7069 fn user_and_admin_settings_round_trip() {
7070 let app_root = temp_app_root("settings");
7071 let result = (|| -> Result<()> {
7072 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
7073 repository.set_user_settings(
7074 &serde_json::json!({
7075 "visual": {
7076 "progressBar": "retro"
7077 }
7078 }),
7079 false,
7080 )?;
7081 repository.set_user_settings(
7082 &serde_json::json!({
7083 "experimentalFeatures": {
7084 "directMSI": true
7085 }
7086 }),
7087 true,
7088 )?;
7089
7090 let user_settings = repository.get_user_settings()?;
7091 assert_eq!(
7092 user_settings["visual"]["progressBar"],
7093 serde_json::Value::String("retro".to_owned())
7094 );
7095 assert_eq!(
7096 user_settings["experimentalFeatures"]["directMSI"],
7097 serde_json::Value::Bool(true)
7098 );
7099 assert!(repository.test_user_settings(
7100 &serde_json::json!({
7101 "experimentalFeatures": {
7102 "directMSI": true
7103 }
7104 }),
7105 true
7106 )?);
7107
7108 repository.set_admin_setting("LocalManifestFiles", true)?;
7109 repository.set_admin_setting("InstallerHashOverride", true)?;
7110 let admin_settings = repository.get_admin_settings()?;
7111 assert_eq!(admin_settings["LocalManifestFiles"], serde_json::Value::Bool(true));
7112 assert_eq!(admin_settings["InstallerHashOverride"], serde_json::Value::Bool(true));
7113
7114 repository.reset_admin_setting(Some("LocalManifestFiles"), false)?;
7115 let reset_one = repository.get_admin_settings()?;
7116 assert_eq!(reset_one["LocalManifestFiles"], serde_json::Value::Bool(false));
7117
7118 repository.reset_admin_setting(None, true)?;
7119 let reset_all = repository.get_admin_settings()?;
7120 for name in Repository::supported_admin_settings() {
7121 assert_eq!(reset_all[*name], serde_json::Value::Bool(false));
7122 }
7123 Ok(())
7124 })();
7125
7126 let _ = fs::remove_dir_all(&app_root);
7127 result.expect("settings");
7128 }
7129
7130 #[test]
7131 fn show_result_structured_document_is_manifest_oriented() {
7132 let result = ShowResult {
7133 package: SearchMatch {
7134 source_name: "winget".to_owned(),
7135 source_kind: SourceKind::PreIndexed,
7136 id: "Test.Package".to_owned(),
7137 name: "Test Package".to_owned(),
7138 moniker: Some("testpkg".to_owned()),
7139 version: Some("1.2.3".to_owned()),
7140 channel: Some("stable".to_owned()),
7141 match_criteria: Some("Id".to_owned()),
7142 },
7143 manifest: Manifest {
7144 id: "Test.Package".to_owned(),
7145 name: "Test Package".to_owned(),
7146 version: "1.2.3".to_owned(),
7147 channel: "stable".to_owned(),
7148 publisher: Some("Contoso".to_owned()),
7149 description: Some("Structured output".to_owned()),
7150 moniker: Some("testpkg".to_owned()),
7151 package_url: None,
7152 publisher_url: None,
7153 publisher_support_url: None,
7154 license: None,
7155 license_url: None,
7156 privacy_url: None,
7157 author: None,
7158 copyright: None,
7159 copyright_url: None,
7160 release_notes: None,
7161 release_notes_url: None,
7162 tags: vec!["utility".to_owned()],
7163 agreements: Vec::new(),
7164 package_dependencies: vec!["Microsoft.VCRedist.2015+.x64".to_owned()],
7165 documentation: vec![Documentation {
7166 label: Some("Docs".to_owned()),
7167 url: "https://example.test/docs".to_owned(),
7168 }],
7169 installers: vec![Installer {
7170 architecture: Some("x64".to_owned()),
7171 installer_type: Some("msix".to_owned()),
7172 url: Some("https://example.test/Test.Package.msix".to_owned()),
7173 sha256: Some("ABC123".to_owned()),
7174 product_code: None,
7175 locale: Some("en-US".to_owned()),
7176 scope: Some("machine".to_owned()),
7177 release_date: None,
7178 package_family_name: None,
7179 upgrade_code: None,
7180 platforms: Vec::new(),
7181 minimum_os_version: None,
7182 switches: InstallerSwitches {
7183 silent: Some("/quiet".to_owned()),
7184 ..InstallerSwitches::default()
7185 },
7186 commands: vec!["testpkg".to_owned()],
7187 package_dependencies: vec!["Microsoft.UI.Xaml.2.8".to_owned()],
7188 }],
7189 },
7190 selected_installer: Some(Installer {
7191 architecture: Some("x64".to_owned()),
7192 installer_type: Some("msix".to_owned()),
7193 url: Some("https://example.test/Test.Package.msix".to_owned()),
7194 sha256: Some("ABC123".to_owned()),
7195 product_code: None,
7196 locale: Some("en-US".to_owned()),
7197 scope: Some("machine".to_owned()),
7198 release_date: None,
7199 package_family_name: None,
7200 upgrade_code: None,
7201 platforms: Vec::new(),
7202 minimum_os_version: None,
7203 switches: InstallerSwitches {
7204 silent: Some("/quiet".to_owned()),
7205 ..InstallerSwitches::default()
7206 },
7207 commands: vec!["testpkg".to_owned()],
7208 package_dependencies: vec!["Microsoft.UI.Xaml.2.8".to_owned()],
7209 }),
7210 cached_files: vec![PathBuf::from(r"C:\temp\cache\Test.Package.yaml")],
7211 warnings: vec!["cache warmed".to_owned()],
7212 manifest_documents: JsonValue::Array(vec![
7213 serde_json::json!({
7214 "PackageIdentifier": "Test.Package",
7215 "PackageVersion": "1.2.3",
7216 "DefaultLocale": "en-US",
7217 "ManifestType": "version",
7218 "ManifestVersion": "1.10.0"
7219 }),
7220 serde_json::json!({
7221 "PackageIdentifier": "Test.Package",
7222 "PackageVersion": "1.2.3",
7223 "PackageLocale": "en-US",
7224 "PackageName": "Test Package",
7225 "Publisher": "Example",
7226 "License": "MIT",
7227 "ShortDescription": "Structured output",
7228 "ManifestType": "defaultLocale",
7229 "ManifestVersion": "1.10.0"
7230 }),
7231 serde_json::json!({
7232 "PackageIdentifier": "Test.Package",
7233 "PackageVersion": "1.2.3",
7234 "ManifestType": "installer",
7235 "ManifestVersion": "1.10.0",
7236 "Installers": [
7237 {
7238 "Architecture": "x64",
7239 "InstallerType": "msix",
7240 "InstallerUrl": "https://example.test/Test.Package.msix",
7241 "InstallerSha256": "ABC123",
7242 "Commands": ["testpkg"],
7243 "InstallerSwitches": { "Silent": "/quiet" },
7244 "Dependencies": {
7245 "PackageDependencies": [
7246 { "PackageIdentifier": "Microsoft.VCRedist.2015+.x64" }
7247 ]
7248 }
7249 }
7250 ]
7251 }),
7252 ]),
7253 };
7254
7255 let document = result.structured_document();
7256
7257 assert_eq!(document["ManifestType"].as_str(), Some("singleton"));
7258 assert_eq!(document["ManifestVersion"].as_str(), Some("1.10.0"));
7259 assert_eq!(document["PackageLocale"].as_str(), Some("en-US"));
7260 assert_eq!(
7261 document["Installers"][0]["Dependencies"]["PackageDependencies"][0]["PackageIdentifier"].as_str(),
7262 Some("Microsoft.VCRedist.2015+.x64")
7263 );
7264 assert_eq!(document["Installers"][0]["Commands"][0].as_str(), Some("testpkg"));
7265 assert_eq!(
7266 document["Installers"][0]["InstallerSwitches"]["Silent"].as_str(),
7267 Some("/quiet")
7268 );
7269 }
7270
7271 #[test]
7272 fn parse_yaml_manifest_bundle_returns_singleton_document() {
7273 let yaml = r#"
7274PackageIdentifier: Test.Package
7275PackageVersion: 1.2.3
7276DefaultLocale: en-US
7277ManifestType: version
7278ManifestVersion: 1.10.0
7279---
7280PackageIdentifier: Test.Package
7281PackageVersion: 1.2.3
7282PackageLocale: en-US
7283PackageName: Test Package
7284Publisher: Example
7285License: MIT
7286ShortDescription: Structured output
7287ManifestType: defaultLocale
7288ManifestVersion: 1.10.0
7289---
7290PackageIdentifier: Test.Package
7291PackageVersion: 1.2.3
7292ManifestType: installer
7293ManifestVersion: 1.10.0
7294Installers:
7295 - Architecture: x64
7296 InstallerType: exe
7297 InstallerUrl: https://example.test/Test.Package.exe
7298 InstallerSha256: ABC123
7299"#;
7300
7301 let (_manifest, documents) = parse_yaml_manifest_bundle(yaml.as_bytes()).expect("bundle");
7302
7303 assert_eq!(documents["ManifestType"].as_str(), Some("singleton"));
7304 assert_eq!(documents["PackageIdentifier"].as_str(), Some("Test.Package"));
7305 assert_eq!(documents["PackageName"].as_str(), Some("Test Package"));
7306 }
7307
7308 #[test]
7309 fn collapse_structured_documents_returns_plural_show_documents() {
7310 let documents = collapse_structured_documents(&[
7311 JsonValue::Array(vec![
7312 serde_json::json!({
7313 "PackageIdentifier": "Test.Package.One",
7314 "PackageVersion": "1.0.0",
7315 "DefaultLocale": "en-US",
7316 "ManifestType": "version",
7317 "ManifestVersion": "1.10.0"
7318 }),
7319 serde_json::json!({
7320 "PackageIdentifier": "Test.Package.One",
7321 "PackageVersion": "1.0.0",
7322 "PackageLocale": "en-US",
7323 "PackageName": "Test Package One",
7324 "ManifestType": "defaultLocale",
7325 "ManifestVersion": "1.10.0"
7326 }),
7327 serde_json::json!({
7328 "PackageIdentifier": "Test.Package.One",
7329 "PackageVersion": "1.0.0",
7330 "ManifestType": "installer",
7331 "ManifestVersion": "1.10.0",
7332 "Installers": [
7333 {
7334 "Architecture": "x64",
7335 "InstallerType": "exe",
7336 "InstallerUrl": "https://example.test/one.exe",
7337 "InstallerSha256": "ABC123"
7338 }
7339 ]
7340 }),
7341 ]),
7342 serde_json::json!({
7343 "PackageIdentifier": "Test.Package.Two",
7344 "PackageVersion": "2.0.0",
7345 "PackageLocale": "en-US",
7346 "PackageName": "Test Package Two",
7347 "ManifestType": "singleton",
7348 "ManifestVersion": "1.12.0"
7349 }),
7350 ]);
7351
7352 assert_eq!(documents.len(), 2);
7353 assert_eq!(documents[0]["ManifestType"].as_str(), Some("singleton"));
7354 assert_eq!(documents[0]["PackageIdentifier"].as_str(), Some("Test.Package.One"));
7355 assert_eq!(documents[0]["PackageName"].as_str(), Some("Test Package One"));
7356 assert_eq!(documents[1]["ManifestType"].as_str(), Some("singleton"));
7357 assert_eq!(documents[1]["PackageIdentifier"].as_str(), Some("Test.Package.Two"));
7358 }
7359
7360 #[test]
7361 fn unsupported_action_result_marks_noop_and_warning() {
7362 let result = unsupported_action_result("Contoso.App", "1.2.3", "install", INSTALL_UNSUPPORTED_WARNING);
7363
7364 assert!(result.success);
7365 assert!(result.no_op);
7366 assert_eq!(result.exit_code, 0);
7367 assert_eq!(result.warnings, vec![INSTALL_UNSUPPORTED_WARNING.to_owned()]);
7368 }
7369
7370 #[test]
7371 fn create_install_no_op_result_honors_no_upgrade() {
7372 let mut request = InstallRequest::new(PackageQuery {
7373 id: Some("Contoso.App".to_owned()),
7374 ..PackageQuery::default()
7375 });
7376 request.no_upgrade = true;
7377
7378 let manifest = Manifest {
7379 id: "Contoso.App".to_owned(),
7380 name: "Contoso App".to_owned(),
7381 version: "2.0.0".to_owned(),
7382 channel: String::new(),
7383 publisher: None,
7384 description: None,
7385 moniker: None,
7386 package_url: None,
7387 publisher_url: None,
7388 publisher_support_url: None,
7389 license: None,
7390 license_url: None,
7391 privacy_url: None,
7392 author: None,
7393 copyright: None,
7394 copyright_url: None,
7395 release_notes: None,
7396 release_notes_url: None,
7397 tags: Vec::new(),
7398 agreements: Vec::new(),
7399 package_dependencies: Vec::new(),
7400 documentation: Vec::new(),
7401 installers: Vec::new(),
7402 };
7403 let existing = ListMatch {
7404 name: "Contoso App".to_owned(),
7405 id: "Contoso.App".to_owned(),
7406 local_id: "Contoso.App".to_owned(),
7407 installed_version: "1.0.0".to_owned(),
7408 available_version: Some("2.0.0".to_owned()),
7409 source_name: Some("winget".to_owned()),
7410 publisher: None,
7411 scope: None,
7412 installer_category: None,
7413 install_location: None,
7414 package_family_names: Vec::new(),
7415 product_codes: Vec::new(),
7416 upgrade_codes: Vec::new(),
7417 };
7418
7419 let result =
7420 Repository::create_install_no_op_result(&request, &manifest, Some(&existing)).expect("no-op result");
7421
7422 assert!(result.success);
7423 assert!(result.no_op);
7424 assert_eq!(result.version, "1.0.0");
7425 assert_eq!(
7426 result.warnings,
7427 vec!["Package is already installed; skipping because --no-upgrade was specified.".to_owned()]
7428 );
7429 }
7430
7431 #[test]
7432 fn create_install_no_op_result_skips_reinstall_when_current() {
7433 let request = InstallRequest::new(PackageQuery {
7434 id: Some("Contoso.App".to_owned()),
7435 ..PackageQuery::default()
7436 });
7437
7438 let manifest = Manifest {
7439 id: "Contoso.App".to_owned(),
7440 name: "Contoso App".to_owned(),
7441 version: "2.0.0".to_owned(),
7442 channel: String::new(),
7443 publisher: None,
7444 description: None,
7445 moniker: None,
7446 package_url: None,
7447 publisher_url: None,
7448 publisher_support_url: None,
7449 license: None,
7450 license_url: None,
7451 privacy_url: None,
7452 author: None,
7453 copyright: None,
7454 copyright_url: None,
7455 release_notes: None,
7456 release_notes_url: None,
7457 tags: Vec::new(),
7458 agreements: Vec::new(),
7459 package_dependencies: Vec::new(),
7460 documentation: Vec::new(),
7461 installers: Vec::new(),
7462 };
7463 let existing = ListMatch {
7464 name: "Contoso App".to_owned(),
7465 id: "Contoso.App".to_owned(),
7466 local_id: "Contoso.App".to_owned(),
7467 installed_version: "2.0.0".to_owned(),
7468 available_version: Some("2.0.0".to_owned()),
7469 source_name: Some("winget".to_owned()),
7470 publisher: None,
7471 scope: None,
7472 installer_category: None,
7473 install_location: None,
7474 package_family_names: Vec::new(),
7475 product_codes: Vec::new(),
7476 upgrade_codes: Vec::new(),
7477 };
7478
7479 let result =
7480 Repository::create_install_no_op_result(&request, &manifest, Some(&existing)).expect("no-op result");
7481
7482 assert!(result.success);
7483 assert!(result.no_op);
7484 assert_eq!(result.version, "2.0.0");
7485 assert_eq!(
7486 result.warnings,
7487 vec!["Package is already installed and up to date; rerun with --force to reinstall.".to_owned()]
7488 );
7489 }
7490
7491 #[test]
7492 fn collapse_structured_document_projects_merged_manifest() {
7493 let document = collapse_structured_document(&serde_json::json!({
7494 "PackageIdentifier": "Test.Package",
7495 "PackageVersion": "1.2.3",
7496 "PackageLocale": "en-US",
7497 "PackageName": "Test Package",
7498 "Publisher": "Example",
7499 "InstallerType": "exe",
7500 "Installers": [
7501 {
7502 "Architecture": "x64",
7503 "InstallerUrl": "https://example.test/Test.Package.exe",
7504 "InstallerSha256": "ABC123"
7505 }
7506 ],
7507 "ManifestType": "merged",
7508 "ManifestVersion": "1.10.0"
7509 }));
7510
7511 assert_eq!(document["ManifestType"].as_str(), Some("singleton"));
7512 assert_eq!(document["PackageIdentifier"].as_str(), Some("Test.Package"));
7513 assert_eq!(document["PackageName"].as_str(), Some("Test Package"));
7514 assert_eq!(document["InstallerType"].as_str(), Some("exe"));
7515 assert_eq!(document["Installers"][0]["Architecture"].as_str(), Some("x64"));
7516 }
7517
7518 #[test]
7519 fn parses_fixture_manifest() {
7520 let root = Path::new(env!("CARGO_MANIFEST_DIR"))
7521 .join("tests")
7522 .join("fixtures")
7523 .join("ManifestV1_28-Singleton.yaml");
7524 let bytes = fs::read(root).expect("fixture bytes");
7525 let manifest = parse_yaml_manifest(&bytes).expect("manifest");
7526 assert_eq!(manifest.id, "microsoft.msixsdk");
7527 assert_eq!(manifest.version, "1.7.32");
7528 assert_eq!(manifest.name, "MSIX SDK");
7529 assert!(!manifest.installers.is_empty());
7530 }
7531
7532 #[test]
7533 fn parses_installer_switches_from_yaml_manifest() {
7534 let yaml = r#"
7535PackageIdentifier: Test.Package
7536PackageVersion: 1.2.3
7537PackageName: Test Package
7538InstallerSwitches:
7539 SilentWithProgress: /SILENT
7540Installers:
7541 - Architecture: x64
7542 InstallerType: inno
7543 InstallerUrl: https://example.test/Test.Package.exe
7544 InstallerSha256: ABC123
7545 InstallerSwitches:
7546 Silent: /VERYSILENT
7547 Interactive: /HELP
7548"#;
7549
7550 let manifest = parse_yaml_manifest(yaml.as_bytes()).expect("manifest");
7551 let installer = &manifest.installers[0];
7552
7553 assert_eq!(installer.switches.silent_with_progress.as_deref(), Some("/SILENT"));
7554 assert_eq!(installer.switches.silent.as_deref(), Some("/VERYSILENT"));
7555 assert_eq!(installer.switches.interactive.as_deref(), Some("/HELP"));
7556 }
7557
7558 #[test]
7559 fn parses_platform_and_minimum_os_version_from_yaml_manifest() {
7560 let yaml = r#"
7561PackageIdentifier: Test.Package
7562PackageVersion: 1.2.3
7563PackageName: Test Package
7564Platform:
7565 - Windows.Desktop
7566MinimumOSVersion: 10.0.19041.0
7567Installers:
7568 - Architecture: x64
7569 InstallerType: exe
7570 InstallerUrl: https://example.test/Test.Package.exe
7571 InstallerSha256: ABC123
7572 - Architecture: x64
7573 InstallerType: msix
7574 Platform:
7575 - Windows.Universal
7576 MinimumOSVersion: 10.0.22621.0
7577 InstallerUrl: https://example.test/Test.Package.msix
7578 InstallerSha256: DEF456
7579"#;
7580
7581 let manifest = parse_yaml_manifest(yaml.as_bytes()).expect("manifest");
7582
7583 assert_eq!(manifest.installers[0].platforms, vec!["Windows.Desktop".to_owned()]);
7584 assert_eq!(
7585 manifest.installers[0].minimum_os_version.as_deref(),
7586 Some("10.0.19041.0")
7587 );
7588 assert_eq!(manifest.installers[1].platforms, vec!["Windows.Universal".to_owned()]);
7589 assert_eq!(
7590 manifest.installers[1].minimum_os_version.as_deref(),
7591 Some("10.0.22621.0")
7592 );
7593 }
7594
7595 #[test]
7596 fn installer_switch_arguments_prefer_manifest_switches() {
7597 let installer = Installer {
7598 architecture: None,
7599 installer_type: Some("inno".to_owned()),
7600 url: None,
7601 sha256: None,
7602 product_code: None,
7603 locale: None,
7604 scope: None,
7605 release_date: None,
7606 package_family_name: None,
7607 upgrade_code: None,
7608 platforms: Vec::new(),
7609 minimum_os_version: None,
7610 switches: InstallerSwitches {
7611 silent: Some("/mysilent".to_owned()),
7612 silent_with_progress: Some("/mysilentwithprogress".to_owned()),
7613 interactive: Some("/myinteractive".to_owned()),
7614 ..InstallerSwitches::default()
7615 },
7616 commands: Vec::new(),
7617 package_dependencies: Vec::new(),
7618 };
7619 let manifest = Manifest {
7620 id: "Test.Package".to_owned(),
7621 name: "Test Package".to_owned(),
7622 version: "1.0.0".to_owned(),
7623 channel: String::new(),
7624 publisher: None,
7625 description: None,
7626 moniker: None,
7627 package_url: None,
7628 publisher_url: None,
7629 publisher_support_url: None,
7630 license: None,
7631 license_url: None,
7632 privacy_url: None,
7633 author: None,
7634 copyright: None,
7635 copyright_url: None,
7636 release_notes: None,
7637 release_notes_url: None,
7638 tags: Vec::new(),
7639 agreements: Vec::new(),
7640 package_dependencies: Vec::new(),
7641 documentation: Vec::new(),
7642 installers: Vec::new(),
7643 };
7644
7645 let mut silent_request = InstallRequest::new(PackageQuery::default());
7646 silent_request.mode = InstallerMode::Silent;
7647 let mut progress_request = InstallRequest::new(PackageQuery::default());
7648 progress_request.mode = InstallerMode::SilentWithProgress;
7649 let mut interactive_request = InstallRequest::new(PackageQuery::default());
7650 interactive_request.mode = InstallerMode::Interactive;
7651 assert_eq!(
7652 installer_command_arguments(
7653 "inno",
7654 &silent_request,
7655 &manifest,
7656 Path::new("installer.exe"),
7657 &installer
7658 ),
7659 vec!["/mysilent".to_owned()]
7660 );
7661 assert_eq!(
7662 installer_command_arguments(
7663 "inno",
7664 &progress_request,
7665 &manifest,
7666 Path::new("installer.exe"),
7667 &installer
7668 ),
7669 vec!["/mysilentwithprogress".to_owned()]
7670 );
7671 assert_eq!(
7672 installer_command_arguments(
7673 "inno",
7674 &interactive_request,
7675 &manifest,
7676 Path::new("installer.exe"),
7677 &installer
7678 ),
7679 vec!["/myinteractive".to_owned()]
7680 );
7681 }
7682
7683 #[test]
7684 fn installer_switch_arguments_use_inno_defaults_without_manifest_switches() {
7685 let installer = Installer {
7686 architecture: None,
7687 installer_type: Some("inno".to_owned()),
7688 url: None,
7689 sha256: None,
7690 product_code: None,
7691 locale: None,
7692 scope: None,
7693 release_date: None,
7694 package_family_name: None,
7695 upgrade_code: None,
7696 platforms: Vec::new(),
7697 minimum_os_version: None,
7698 switches: InstallerSwitches::default(),
7699 commands: Vec::new(),
7700 package_dependencies: Vec::new(),
7701 };
7702 let manifest = Manifest {
7703 id: "Test.Package".to_owned(),
7704 name: "Test Package".to_owned(),
7705 version: "1.0.0".to_owned(),
7706 channel: String::new(),
7707 publisher: None,
7708 description: None,
7709 moniker: None,
7710 package_url: None,
7711 publisher_url: None,
7712 publisher_support_url: None,
7713 license: None,
7714 license_url: None,
7715 privacy_url: None,
7716 author: None,
7717 copyright: None,
7718 copyright_url: None,
7719 release_notes: None,
7720 release_notes_url: None,
7721 tags: Vec::new(),
7722 agreements: Vec::new(),
7723 package_dependencies: Vec::new(),
7724 documentation: Vec::new(),
7725 installers: Vec::new(),
7726 };
7727 let mut progress_request = InstallRequest::new(PackageQuery::default());
7728 progress_request.mode = InstallerMode::SilentWithProgress;
7729 let mut silent_request = InstallRequest::new(PackageQuery::default());
7730 silent_request.mode = InstallerMode::Silent;
7731
7732 assert_eq!(
7733 installer_command_arguments(
7734 "inno",
7735 &progress_request,
7736 &manifest,
7737 Path::new("installer.exe"),
7738 &installer
7739 ),
7740 vec![
7741 "/SP-".to_owned(),
7742 "/SILENT".to_owned(),
7743 "/SUPPRESSMSGBOXES".to_owned(),
7744 "/NORESTART".to_owned()
7745 ]
7746 );
7747 assert_eq!(
7748 installer_command_arguments(
7749 "inno",
7750 &silent_request,
7751 &manifest,
7752 Path::new("installer.exe"),
7753 &installer
7754 ),
7755 vec![
7756 "/SP-".to_owned(),
7757 "/VERYSILENT".to_owned(),
7758 "/SUPPRESSMSGBOXES".to_owned(),
7759 "/NORESTART".to_owned()
7760 ]
7761 );
7762 }
7763
7764 #[test]
7765 fn installer_command_arguments_append_manifest_and_cli_switches() {
7766 let installer = Installer {
7767 architecture: None,
7768 installer_type: Some("msi".to_owned()),
7769 url: None,
7770 sha256: None,
7771 product_code: None,
7772 locale: None,
7773 scope: None,
7774 release_date: None,
7775 package_family_name: None,
7776 upgrade_code: None,
7777 platforms: Vec::new(),
7778 minimum_os_version: None,
7779 switches: InstallerSwitches {
7780 custom: Some("ADDLOCAL=Core".to_owned()),
7781 log: Some("/log \"<LOGPATH>\"".to_owned()),
7782 install_location: Some("TARGETDIR=\"<INSTALLPATH>\"".to_owned()),
7783 ..InstallerSwitches::default()
7784 },
7785 commands: Vec::new(),
7786 package_dependencies: Vec::new(),
7787 };
7788 let manifest = Manifest {
7789 id: "ShareX.ShareX".to_owned(),
7790 name: "ShareX".to_owned(),
7791 version: "19.0.2".to_owned(),
7792 channel: String::new(),
7793 publisher: None,
7794 description: None,
7795 moniker: None,
7796 package_url: None,
7797 publisher_url: None,
7798 publisher_support_url: None,
7799 license: None,
7800 license_url: None,
7801 privacy_url: None,
7802 author: None,
7803 copyright: None,
7804 copyright_url: None,
7805 release_notes: None,
7806 release_notes_url: None,
7807 tags: Vec::new(),
7808 agreements: Vec::new(),
7809 package_dependencies: Vec::new(),
7810 documentation: Vec::new(),
7811 installers: Vec::new(),
7812 };
7813 let mut request = InstallRequest::new(PackageQuery::default());
7814 request.mode = InstallerMode::Silent;
7815 request.log_path = Some(PathBuf::from(r"C:\temp\winget.log"));
7816 request.custom = Some("REBOOT=ReallySuppress".to_owned());
7817 request.install_location = Some(r"C:\Apps\ShareX".to_owned());
7818
7819 assert_eq!(
7820 installer_command_arguments("msi", &request, &manifest, Path::new(r"C:\temp\ShareX.msi"), &installer),
7821 vec![
7822 "/i".to_owned(),
7823 r"C:\temp\ShareX.msi".to_owned(),
7824 "/quiet".to_owned(),
7825 "/norestart".to_owned(),
7826 "/log".to_owned(),
7827 r"C:\temp\winget.log".to_owned(),
7828 "ADDLOCAL=Core".to_owned(),
7829 "REBOOT=ReallySuppress".to_owned(),
7830 r"TARGETDIR=C:\Apps\ShareX".to_owned(),
7831 ]
7832 );
7833 }
7834
7835 #[test]
7836 fn decompresses_mszyml_payload() {
7837 let payload = "sV: 1.0.0\nvD:\n - v: 1.2.3\n rP: manifests/test.yaml\n s256H: ABCD\n";
7838 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
7839 encoder.write_all(payload.as_bytes()).expect("write payload");
7840 let compressed = encoder.finish().expect("finish payload");
7841 let mut mszip = b"CK".to_vec();
7842 mszip.extend_from_slice(&compressed);
7843
7844 let decompressed = decompress_mszyml(&mszip).expect("decompress");
7845 let parsed = serde_yaml::from_str::<PackageVersionDataDocument>(&decompressed).expect("parse");
7846 assert_eq!(parsed.versions[0].version, "1.2.3");
7847 }
7848
7849 #[test]
7850 fn decompresses_mszyml_payload_with_prefix() {
7851 let payload = "sV: 1.0.0\nvD:\n - v: 1.2.3\n rP: manifests/test.yaml\n s256H: ABCD\n";
7852 let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
7853 encoder.write_all(payload.as_bytes()).expect("write payload");
7854 let compressed = encoder.finish().expect("finish payload");
7855 let mut prefixed = vec![0u8; 28];
7856 prefixed.extend_from_slice(b"CK");
7857 prefixed.extend_from_slice(&compressed);
7858
7859 let decompressed = decompress_mszyml(&prefixed).expect("decompress");
7860 let parsed = serde_yaml::from_str::<PackageVersionDataDocument>(&decompressed).expect("parse");
7861 assert_eq!(parsed.versions[0].version, "1.2.3");
7862 }
7863
7864 #[test]
7865 fn compares_versions_naturally() {
7866 assert_eq!(compare_version("1.10.0", "1.9.9"), Ordering::Greater);
7867 assert_eq!(compare_version("2.0", "10.0"), Ordering::Less);
7868 assert_eq!(compare_version("1.0.0-preview2", "1.0.0-preview1"), Ordering::Greater);
7869 }
7870
7871 #[test]
7872 fn installer_hash_verification_fails_on_mismatch_without_override() {
7873 let result = verify_installer_hash(Some("DEADBEEF"), b"pinget", false);
7874 assert!(result.is_err());
7875 assert!(
7876 result
7877 .expect_err("hash mismatch")
7878 .to_string()
7879 .contains("Installer hash mismatch")
7880 );
7881 }
7882
7883 #[test]
7884 fn installer_hash_verification_allows_mismatch_with_override() {
7885 verify_installer_hash(Some("DEADBEEF"), b"pinget", true).expect("hash override");
7886 verify_installer_hash(None, b"pinget", false).expect("missing hash");
7887 }
7888
7889 #[test]
7890 fn search_query_many_includes_tag_and_command_conditions() {
7891 let query = PackageQuery {
7892 query: Some("terminal".to_owned()),
7893 ..PackageQuery::default()
7894 };
7895
7896 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Many);
7897
7898 assert!(where_clause.contains("tags2"));
7899 assert!(where_clause.contains("commands2"));
7900 assert_eq!(params.len(), 5);
7901 }
7902
7903 #[test]
7904 fn show_query_single_omits_tag_and_command_conditions() {
7905 let query = PackageQuery {
7906 query: Some("terminal".to_owned()),
7907 ..PackageQuery::default()
7908 };
7909
7910 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Single);
7911
7912 assert!(!where_clause.contains("tags2"));
7913 assert!(!where_clause.contains("commands2"));
7914 assert_eq!(params.len(), 3);
7915 }
7916
7917 #[test]
7918 fn explicit_tag_filter_uses_exact_match() {
7919 let query = PackageQuery {
7920 tag: Some("terminal".to_owned()),
7921 ..PackageQuery::default()
7922 };
7923
7924 let (where_clause, params) = build_preindexed_where_clause(&query, true, SearchSemantics::Many);
7925
7926 assert!(where_clause.contains("tags2"));
7927 assert_eq!(params, vec!["terminal".to_owned()]);
7928 }
7929
7930 #[test]
7931 fn rest_tag_filter_uses_exact_match() {
7932 let query = PackageQuery {
7933 tag: Some("terminal".to_owned()),
7934 ..PackageQuery::default()
7935 };
7936 let info = RestInformation {
7937 required_package_match_fields: Vec::new(),
7938 unsupported_package_match_fields: Vec::new(),
7939 required_query_parameters: Vec::new(),
7940 ..RestInformation::default()
7941 };
7942
7943 let body = build_rest_search_body(&query, &info, SearchSemantics::Many).expect("rest body");
7944 let filters = body["Filters"].as_array().expect("filters");
7945
7946 assert_eq!(filters.len(), 1);
7947 assert_eq!(filters[0]["PackageMatchField"], "Tag");
7948 assert_eq!(filters[0]["RequestMatch"]["MatchType"], "Exact");
7949 assert_eq!(filters[0]["RequestMatch"]["KeyWord"], "terminal");
7950 }
7951
7952 #[test]
7953 fn selects_installer_using_requested_filters() {
7954 let installers = vec![
7955 Installer {
7956 architecture: Some("x86".to_owned()),
7957 installer_type: Some("zip".to_owned()),
7958 url: None,
7959 sha256: None,
7960 product_code: None,
7961 locale: Some("en-US".to_owned()),
7962 scope: Some("user".to_owned()),
7963 release_date: None,
7964 package_family_name: None,
7965 upgrade_code: None,
7966 platforms: Vec::new(),
7967 minimum_os_version: None,
7968 switches: InstallerSwitches::default(),
7969 commands: Vec::new(),
7970 package_dependencies: Vec::new(),
7971 },
7972 Installer {
7973 architecture: Some("x64".to_owned()),
7974 installer_type: Some("msix".to_owned()),
7975 url: None,
7976 sha256: None,
7977 product_code: None,
7978 locale: Some("en-US".to_owned()),
7979 scope: Some("user".to_owned()),
7980 release_date: None,
7981 package_family_name: None,
7982 upgrade_code: None,
7983 platforms: Vec::new(),
7984 minimum_os_version: None,
7985 switches: InstallerSwitches::default(),
7986 commands: vec!["demo".to_owned()],
7987 package_dependencies: Vec::new(),
7988 },
7989 ];
7990 let query = PackageQuery {
7991 installer_type: Some("msix".to_owned()),
7992 installer_architecture: Some("x64".to_owned()),
7993 install_scope: Some("user".to_owned()),
7994 locale: Some("en-US".to_owned()),
7995 ..PackageQuery::default()
7996 };
7997
7998 let selected = select_installer(&installers, &query).expect("selected installer");
7999 assert_eq!(selected.installer_type.as_deref(), Some("msix"));
8000 assert_eq!(selected.architecture.as_deref(), Some("x64"));
8001 }
8002
8003 #[test]
8004 fn selects_first_installer_when_rank_ties() {
8005 let installers = vec![
8006 Installer {
8007 architecture: Some("x64".to_owned()),
8008 installer_type: Some("exe".to_owned()),
8009 url: None,
8010 sha256: None,
8011 product_code: None,
8012 locale: Some("en-US".to_owned()),
8013 scope: Some("machine".to_owned()),
8014 release_date: None,
8015 package_family_name: None,
8016 upgrade_code: None,
8017 platforms: Vec::new(),
8018 minimum_os_version: None,
8019 switches: InstallerSwitches::default(),
8020 commands: Vec::new(),
8021 package_dependencies: Vec::new(),
8022 },
8023 Installer {
8024 architecture: Some("x64".to_owned()),
8025 installer_type: Some("exe".to_owned()),
8026 url: None,
8027 sha256: None,
8028 product_code: None,
8029 locale: Some("en-US".to_owned()),
8030 scope: Some("user".to_owned()),
8031 release_date: None,
8032 package_family_name: None,
8033 upgrade_code: None,
8034 platforms: Vec::new(),
8035 minimum_os_version: None,
8036 switches: InstallerSwitches::default(),
8037 commands: Vec::new(),
8038 package_dependencies: Vec::new(),
8039 },
8040 ];
8041
8042 let selected = select_installer(&installers, &PackageQuery::default()).expect("selected installer");
8043 assert_eq!(selected.scope.as_deref(), Some("machine"));
8044 }
8045
8046 #[test]
8047 fn selects_installer_using_requested_platform() {
8048 let installers = vec![
8049 Installer {
8050 architecture: Some("x64".to_owned()),
8051 installer_type: Some("exe".to_owned()),
8052 url: None,
8053 sha256: None,
8054 product_code: None,
8055 locale: Some("en-US".to_owned()),
8056 scope: Some("user".to_owned()),
8057 release_date: None,
8058 package_family_name: None,
8059 upgrade_code: None,
8060 platforms: vec!["Windows.Universal".to_owned()],
8061 minimum_os_version: None,
8062 switches: InstallerSwitches::default(),
8063 commands: Vec::new(),
8064 package_dependencies: Vec::new(),
8065 },
8066 Installer {
8067 architecture: Some("x64".to_owned()),
8068 installer_type: Some("exe".to_owned()),
8069 url: None,
8070 sha256: None,
8071 product_code: None,
8072 locale: Some("en-US".to_owned()),
8073 scope: Some("user".to_owned()),
8074 release_date: None,
8075 package_family_name: None,
8076 upgrade_code: None,
8077 platforms: vec!["Windows.Desktop".to_owned()],
8078 minimum_os_version: None,
8079 switches: InstallerSwitches::default(),
8080 commands: Vec::new(),
8081 package_dependencies: Vec::new(),
8082 },
8083 ];
8084 let query = PackageQuery {
8085 installer_type: Some("exe".to_owned()),
8086 platform: Some("Windows.Desktop".to_owned()),
8087 ..PackageQuery::default()
8088 };
8089
8090 let selected = select_installer(&installers, &query).expect("selected installer");
8091 assert_eq!(selected.platforms, vec!["Windows.Desktop".to_owned()]);
8092 }
8093
8094 #[test]
8095 fn selects_installer_using_requested_os_version() {
8096 let installers = vec![
8097 Installer {
8098 architecture: Some("x64".to_owned()),
8099 installer_type: Some("exe".to_owned()),
8100 url: None,
8101 sha256: None,
8102 product_code: None,
8103 locale: Some("en-US".to_owned()),
8104 scope: Some("user".to_owned()),
8105 release_date: None,
8106 package_family_name: None,
8107 upgrade_code: None,
8108 platforms: Vec::new(),
8109 minimum_os_version: Some("10.0.22621.0".to_owned()),
8110 switches: InstallerSwitches::default(),
8111 commands: Vec::new(),
8112 package_dependencies: Vec::new(),
8113 },
8114 Installer {
8115 architecture: Some("x64".to_owned()),
8116 installer_type: Some("exe".to_owned()),
8117 url: None,
8118 sha256: None,
8119 product_code: None,
8120 locale: Some("en-US".to_owned()),
8121 scope: Some("user".to_owned()),
8122 release_date: None,
8123 package_family_name: None,
8124 upgrade_code: None,
8125 platforms: Vec::new(),
8126 minimum_os_version: Some("10.0.19041.0".to_owned()),
8127 switches: InstallerSwitches::default(),
8128 commands: Vec::new(),
8129 package_dependencies: Vec::new(),
8130 },
8131 ];
8132 let query = PackageQuery {
8133 installer_type: Some("exe".to_owned()),
8134 os_version: Some("10.0.19045.0".to_owned()),
8135 ..PackageQuery::default()
8136 };
8137
8138 let selected = select_installer(&installers, &query).expect("selected installer");
8139 assert_eq!(selected.minimum_os_version.as_deref(), Some("10.0.19041.0"));
8140 }
8141
8142 #[test]
8143 fn derives_correlation_name_candidates() {
8144 let candidates = correlation_name_candidates("PowerToys (Preview) x64");
8145 assert!(candidates.contains(&"PowerToys (Preview) x64".to_owned()));
8146 assert!(candidates.contains(&"PowerToys".to_owned()));
8147 }
8148
8149 #[test]
8150 fn correlates_installed_package_to_available_match() {
8151 let installed = InstalledPackage {
8152 name: "PowerToys (Preview) x64".to_owned(),
8153 local_id: r"ARP\Machine\X64\PowerToys".to_owned(),
8154 installed_version: "0.98.1".to_owned(),
8155 publisher: None,
8156 scope: Some("Machine".to_owned()),
8157 installer_category: Some("exe".to_owned()),
8158 install_location: None,
8159 package_family_names: Vec::new(),
8160 product_codes: Vec::new(),
8161 upgrade_codes: Vec::new(),
8162 correlated: None,
8163 };
8164 let candidates = vec![SearchMatch {
8165 source_name: "winget".to_owned(),
8166 source_kind: SourceKind::PreIndexed,
8167 id: "Microsoft.PowerToys".to_owned(),
8168 name: "PowerToys".to_owned(),
8169 moniker: None,
8170 version: Some("0.98.1".to_owned()),
8171 channel: None,
8172 match_criteria: None,
8173 }];
8174
8175 let correlated = correlate_installed_package(&installed, &candidates, true).expect("correlated");
8176 assert_eq!(correlated.id, "Microsoft.PowerToys");
8177 }
8178
8179 #[test]
8180 fn list_query_uses_available_lookup_for_tag_filters() {
8181 let query = ListQuery {
8182 tag: Some("terminal".to_owned()),
8183 ..ListQuery::default()
8184 };
8185
8186 assert!(list_query_needs_available_lookup(&query));
8187 }
8188
8189 #[test]
8190 fn list_tag_filter_requires_correlated_match() {
8191 let package = InstalledPackage {
8192 name: "PowerToys (Preview) x64".to_owned(),
8193 local_id: r"ARP\Machine\X64\PowerToys".to_owned(),
8194 installed_version: "0.98.1".to_owned(),
8195 publisher: None,
8196 scope: Some("Machine".to_owned()),
8197 installer_category: Some("exe".to_owned()),
8198 install_location: None,
8199 package_family_names: Vec::new(),
8200 product_codes: Vec::new(),
8201 upgrade_codes: Vec::new(),
8202 correlated: None,
8203 };
8204 let query = ListQuery {
8205 tag: Some("powertoys".to_owned()),
8206 ..ListQuery::default()
8207 };
8208
8209 assert!(!list_package_matches(&package, &query));
8210 }
8211
8212 #[test]
8213 fn list_lookup_ignores_display_count() {
8214 let query = ListQuery {
8215 query: Some("git".to_owned()),
8216 count: Some(5),
8217 ..ListQuery::default()
8218 };
8219
8220 let package_query = package_query_from_list_query(&query);
8221 assert_eq!(package_query.count, Some(LIST_LOOKUP_MAX_RESULTS));
8222 }
8223
8224 #[test]
8225 fn correlation_tiebreaks_on_unnormalized_name_prefix() {
8226 let installed = InstalledPackage {
8231 name: "Notepad++ (ARM 64-bit)".to_owned(),
8232 local_id: r"ARP\Machine\X64\Notepad++".to_owned(),
8233 installed_version: "8.8.9".to_owned(),
8234 publisher: None,
8235 scope: Some("Machine".to_owned()),
8236 installer_category: Some("exe".to_owned()),
8237 install_location: None,
8238 package_family_names: Vec::new(),
8239 product_codes: Vec::new(),
8240 upgrade_codes: Vec::new(),
8241 correlated: None,
8242 };
8243 let candidates = vec![
8244 SearchMatch {
8245 source_name: "winget".to_owned(),
8246 source_kind: SourceKind::PreIndexed,
8247 id: "ndd.Notepad--".to_owned(),
8248 name: "Notepad--".to_owned(),
8249 moniker: None,
8250 version: Some("1.0.0".to_owned()),
8251 channel: None,
8252 match_criteria: None,
8253 },
8254 SearchMatch {
8255 source_name: "winget".to_owned(),
8256 source_kind: SourceKind::PreIndexed,
8257 id: "Notepad++.Notepad++".to_owned(),
8258 name: "Notepad++".to_owned(),
8259 moniker: None,
8260 version: Some("8.9.5".to_owned()),
8261 channel: None,
8262 match_criteria: None,
8263 },
8264 ];
8265
8266 let correlated = correlate_installed_package(&installed, &candidates, true).expect("correlated");
8267 assert_eq!(correlated.id, "Notepad++.Notepad++");
8268 }
8269
8270 #[test]
8271 fn loose_correlation_prefers_anchored_candidate_over_word_fragment() {
8272 let installed = InstalledPackage {
8277 name: "Microsoft .NET SDK 10.0.101 (arm64) from Visual Studio".to_owned(),
8278 local_id: r"ARP\Machine\X64\{7E9F8584-06E7-445E-9165-7486CC1B56C3}".to_owned(),
8279 installed_version: "40.10.18029".to_owned(),
8280 publisher: None,
8281 scope: Some("Machine".to_owned()),
8282 installer_category: Some("msi".to_owned()),
8283 install_location: None,
8284 package_family_names: Vec::new(),
8285 product_codes: Vec::new(),
8286 upgrade_codes: Vec::new(),
8287 correlated: None,
8288 };
8289 let candidates = vec![
8290 SearchMatch {
8291 source_name: "winget".to_owned(),
8292 source_kind: SourceKind::PreIndexed,
8293 id: "ZeroBrane.Studio".to_owned(),
8294 name: "Studio".to_owned(),
8295 moniker: None,
8296 version: Some("1.0".to_owned()),
8297 channel: None,
8298 match_criteria: None,
8299 },
8300 SearchMatch {
8301 source_name: "winget".to_owned(),
8302 source_kind: SourceKind::PreIndexed,
8303 id: "BrickLink.Studio".to_owned(),
8304 name: "Studio".to_owned(),
8305 moniker: None,
8306 version: Some("1.0".to_owned()),
8307 channel: None,
8308 match_criteria: None,
8309 },
8310 SearchMatch {
8311 source_name: "winget".to_owned(),
8312 source_kind: SourceKind::PreIndexed,
8313 id: "Microsoft.DotNet.SDK.10".to_owned(),
8314 name: "Microsoft .NET SDK 10.0".to_owned(),
8315 moniker: None,
8316 version: Some("10.0.204".to_owned()),
8317 channel: None,
8318 match_criteria: None,
8319 },
8320 ];
8321
8322 let correlated = correlate_installed_package(&installed, &candidates, true).expect("correlated");
8323 assert_eq!(correlated.id, "Microsoft.DotNet.SDK.10");
8324 }
8325
8326 #[test]
8327 fn msix_packages_correlate_by_name() {
8328 let installed = InstalledPackage {
8332 name: "Microsoft Teams".to_owned(),
8333 local_id: r"MSIX\MSTeams_25290.205.4069.4894_arm64__8wekyb3d8bbwe".to_owned(),
8334 installed_version: "25290.205.4069.4894".to_owned(),
8335 publisher: None,
8336 scope: Some("User".to_owned()),
8337 installer_category: Some("msix".to_owned()),
8338 install_location: None,
8339 package_family_names: vec!["MSTeams_8wekyb3d8bbwe".to_owned()],
8340 product_codes: Vec::new(),
8341 upgrade_codes: Vec::new(),
8342 correlated: None,
8343 };
8344 let candidates = vec![SearchMatch {
8345 source_name: "winget".to_owned(),
8346 source_kind: SourceKind::PreIndexed,
8347 id: "Microsoft.Teams".to_owned(),
8348 name: "Microsoft Teams".to_owned(),
8349 moniker: None,
8350 version: Some("26106.1906.4665.7308".to_owned()),
8351 channel: None,
8352 match_criteria: None,
8353 }];
8354
8355 let correlated = correlate_installed_package(&installed, &candidates, true).expect("correlated");
8356 assert_eq!(correlated.id, "Microsoft.Teams");
8357 }
8358
8359 #[test]
8360 fn msix_with_resource_string_name_does_not_correlate() {
8361 let installed = InstalledPackage {
8365 name: "ms-resource:appDisplayName".to_owned(),
8366 local_id: r"MSIX\Microsoft.DesktopAppInstaller_1.28.239.0_arm64__8wekyb3d8bbwe".to_owned(),
8367 installed_version: "1.28.239.0".to_owned(),
8368 publisher: None,
8369 scope: Some("User".to_owned()),
8370 installer_category: Some("msix".to_owned()),
8371 install_location: None,
8372 package_family_names: vec!["Microsoft.DesktopAppInstaller_8wekyb3d8bbwe".to_owned()],
8373 product_codes: Vec::new(),
8374 upgrade_codes: Vec::new(),
8375 correlated: None,
8376 };
8377 let candidates = vec![SearchMatch {
8378 source_name: "winget".to_owned(),
8379 source_kind: SourceKind::PreIndexed,
8380 id: "Microsoft.AppInstaller".to_owned(),
8381 name: "App Installer".to_owned(),
8382 moniker: None,
8383 version: Some("1.28.240.0".to_owned()),
8384 channel: None,
8385 match_criteria: None,
8386 }];
8387
8388 assert!(correlate_installed_package(&installed, &candidates, true).is_none());
8389 }
8390
8391 #[test]
8392 fn strict_list_correlation_avoids_short_substring_matches() {
8393 let installed = InstalledPackage {
8394 name: "GitHub".to_owned(),
8395 local_id: r"ARP\User\X64\GitHub".to_owned(),
8396 installed_version: "1.0.0".to_owned(),
8397 publisher: None,
8398 scope: Some("User".to_owned()),
8399 installer_category: Some("exe".to_owned()),
8400 install_location: None,
8401 package_family_names: Vec::new(),
8402 product_codes: Vec::new(),
8403 upgrade_codes: Vec::new(),
8404 correlated: None,
8405 };
8406 let candidates = vec![SearchMatch {
8407 source_name: "winget".to_owned(),
8408 source_kind: SourceKind::PreIndexed,
8409 id: "Git.Git.PreRelease".to_owned(),
8410 name: "Git".to_owned(),
8411 moniker: None,
8412 version: Some("2.54.0".to_owned()),
8413 channel: None,
8414 match_criteria: None,
8415 }];
8416
8417 assert!(correlate_installed_package(&installed, &candidates, false).is_none());
8418 }
8419
8420 #[test]
8421 fn detects_available_upgrade_from_correlated_package() {
8422 let package = InstalledPackage {
8423 name: "AzCopy v10".to_owned(),
8424 local_id: r"ARP\Machine\X64\AzCopy".to_owned(),
8425 installed_version: "10.32.2".to_owned(),
8426 publisher: None,
8427 scope: Some("Machine".to_owned()),
8428 installer_category: Some("exe".to_owned()),
8429 install_location: None,
8430 package_family_names: Vec::new(),
8431 product_codes: Vec::new(),
8432 upgrade_codes: Vec::new(),
8433 correlated: Some(SearchMatch {
8434 source_name: "winget".to_owned(),
8435 source_kind: SourceKind::PreIndexed,
8436 id: "Microsoft.Azure.AZCopy.10".to_owned(),
8437 name: "AzCopy v10".to_owned(),
8438 moniker: None,
8439 version: Some("10.32.3".to_owned()),
8440 channel: None,
8441 match_criteria: None,
8442 }),
8443 };
8444
8445 assert!(installed_package_has_upgrade(&package));
8446 }
8447
8448 #[test]
8449 fn include_unknown_treats_unknown_version_as_upgradable_when_correlated() {
8450 let package = InstalledPackage {
8451 name: "Example Tool".to_owned(),
8452 local_id: r"ARP\User\X64\ExampleTool".to_owned(),
8453 installed_version: "Unknown".to_owned(),
8454 publisher: None,
8455 scope: Some("User".to_owned()),
8456 installer_category: Some("exe".to_owned()),
8457 install_location: None,
8458 package_family_names: Vec::new(),
8459 product_codes: Vec::new(),
8460 upgrade_codes: Vec::new(),
8461 correlated: Some(SearchMatch {
8462 source_name: "winget".to_owned(),
8463 source_kind: SourceKind::PreIndexed,
8464 id: "Contoso.ExampleTool".to_owned(),
8465 name: "Example Tool".to_owned(),
8466 moniker: None,
8467 version: Some("2.0.0".to_owned()),
8468 channel: None,
8469 match_criteria: None,
8470 }),
8471 };
8472 let query = ListQuery {
8473 upgrade_only: true,
8474 include_unknown: true,
8475 ..Default::default()
8476 };
8477
8478 assert!(installed_package_matches_upgrade_filter(&package, &query));
8479 assert_eq!(
8480 list_match_from_installed(package).available_version.as_deref(),
8481 Some("2.0.0")
8482 );
8483 }
8484
8485 #[test]
8486 fn create_repair_list_query_includes_installed_selectors() {
8487 let request = RepairRequest {
8488 query: PackageQuery {
8489 query: Some("powertoys".to_owned()),
8490 id: Some("Microsoft.PowerToys".to_owned()),
8491 name: Some("PowerToys".to_owned()),
8492 moniker: Some("powertoys".to_owned()),
8493 source: Some("winget".to_owned()),
8494 exact: true,
8495 version: Some("0.98.1".to_owned()),
8496 install_scope: Some("Machine".to_owned()),
8497 ..PackageQuery::default()
8498 },
8499 manifest_path: None,
8500 product_code: Some("{GUID}".to_owned()),
8501 mode: InstallerMode::Silent,
8502 log_path: Some(PathBuf::from("repair.log")),
8503 accept_package_agreements: true,
8504 force: true,
8505 ignore_security_hash: true,
8506 };
8507
8508 let query = Repository::create_repair_list_query(&request);
8509 assert_eq!(query.query.as_deref(), Some("powertoys"));
8510 assert_eq!(query.id.as_deref(), Some("Microsoft.PowerToys"));
8511 assert_eq!(query.name.as_deref(), Some("PowerToys"));
8512 assert_eq!(query.moniker.as_deref(), Some("powertoys"));
8513 assert_eq!(query.product_code.as_deref(), Some("{GUID}"));
8514 assert_eq!(query.version.as_deref(), Some("0.98.1"));
8515 assert_eq!(query.source.as_deref(), Some("winget"));
8516 assert!(query.exact);
8517 assert_eq!(query.install_scope.as_deref(), Some("Machine"));
8518 assert_eq!(query.count, Some(100));
8519 }
8520
8521 #[test]
8522 fn create_repair_install_request_forces_reinstall_of_resolved_installed_package() {
8523 let request = RepairRequest {
8524 query: PackageQuery {
8525 name: Some("PowerToys".to_owned()),
8526 source: Some("winget".to_owned()),
8527 locale: Some("en-US".to_owned()),
8528 installer_architecture: Some("x64".to_owned()),
8529 install_scope: Some("Machine".to_owned()),
8530 ..PackageQuery::default()
8531 },
8532 manifest_path: None,
8533 product_code: None,
8534 mode: InstallerMode::SilentWithProgress,
8535 log_path: Some(PathBuf::from("repair.log")),
8536 accept_package_agreements: true,
8537 force: false,
8538 ignore_security_hash: true,
8539 };
8540 let installed = ListMatch {
8541 name: "PowerToys".to_owned(),
8542 id: "Microsoft.PowerToys".to_owned(),
8543 local_id: r"ARP\Machine\X64\Microsoft.PowerToys".to_owned(),
8544 installed_version: "0.98.1".to_owned(),
8545 available_version: Some("0.98.2".to_owned()),
8546 source_name: Some("winget".to_owned()),
8547 publisher: None,
8548 scope: Some("Machine".to_owned()),
8549 installer_category: Some("exe".to_owned()),
8550 install_location: None,
8551 package_family_names: Vec::new(),
8552 product_codes: Vec::new(),
8553 upgrade_codes: Vec::new(),
8554 };
8555
8556 let install_request = Repository::create_repair_install_request(&request, Some(&installed));
8557 assert_eq!(install_request.query.id.as_deref(), Some("Microsoft.PowerToys"));
8558 assert!(install_request.query.query.is_none());
8559 assert!(install_request.query.name.is_none());
8560 assert!(install_request.query.moniker.is_none());
8561 assert_eq!(install_request.query.source.as_deref(), Some("winget"));
8562 assert_eq!(install_request.query.version.as_deref(), Some("0.98.1"));
8563 assert_eq!(install_request.query.locale.as_deref(), Some("en-US"));
8564 assert_eq!(install_request.query.installer_architecture.as_deref(), Some("x64"));
8565 assert_eq!(install_request.query.install_scope.as_deref(), Some("Machine"));
8566 assert!(install_request.query.exact);
8567 assert_eq!(install_request.mode, InstallerMode::SilentWithProgress);
8568 assert_eq!(install_request.log_path, Some(PathBuf::from("repair.log")));
8569 assert!(install_request.accept_package_agreements);
8570 assert!(install_request.force);
8571 assert!(install_request.ignore_security_hash);
8572 }
8573
8574 #[test]
8575 fn source_scoped_pin_operations_round_trip() {
8576 let app_root = temp_app_root("pins");
8577 let result = (|| -> Result<()> {
8578 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
8579 repository.add_pin("Contoso.Tool", "1.0", "winget", PinType::Pinning)?;
8580 repository.add_pin("Contoso.Tool", "*", "test", PinType::Blocking)?;
8581 repository.add_pin("Fabrikam.Tool", "*", "", PinType::Pinning)?;
8582
8583 assert_eq!(repository.list_pins(None)?.len(), 3);
8584 assert_eq!(repository.list_pins(Some("winget"))?.len(), 1);
8585 assert_eq!(repository.list_pins(Some("test"))?.len(), 1);
8586
8587 assert!(repository.remove_pin("Contoso.Tool", Some("winget"))?);
8588 assert_eq!(repository.list_pins(None)?.len(), 2);
8589 assert!(!repository.remove_pin("Contoso.Tool", Some("winget"))?);
8590
8591 repository.reset_pins(Some("test"))?;
8592 let remaining = repository.list_pins(None)?;
8593 assert_eq!(remaining.len(), 1);
8594 assert_eq!(remaining[0].package_id, "Fabrikam.Tool");
8595
8596 repository.reset_pins(None)?;
8597 assert!(repository.list_pins(None)?.is_empty());
8598 Ok(())
8599 })();
8600
8601 let _ = fs::remove_dir_all(&app_root);
8602 result.expect("pin round trip");
8603 }
8604
8605 #[test]
8606 fn pin_operations_work_with_packaged_pin_schema() {
8607 let app_root = temp_app_root("pins_packaged_schema");
8608 let result = (|| -> Result<()> {
8609 let db_path = pins_db_path(&app_root);
8610 if let Some(parent) = db_path.parent() {
8611 fs::create_dir_all(parent)?;
8612 }
8613
8614 let conn = Connection::open(&db_path)?;
8615 conn.execute_batch(
8616 "CREATE TABLE pin (
8617 package_id TEXT NOT NULL,
8618 source_id TEXT NOT NULL,
8619 type INTEGER NOT NULL,
8620 version TEXT NOT NULL,
8621 PRIMARY KEY (package_id, source_id)
8622 )",
8623 )?;
8624 drop(conn);
8625
8626 let repository = Repository::open_with_options(RepositoryOptions::new(app_root.clone()))?;
8627 repository.add_pin("Contoso.Tool", "1.2.*", "winget", PinType::Gating)?;
8628
8629 let pins = repository.list_pins(None)?;
8630 assert_eq!(pins.len(), 1);
8631 assert_eq!(pins[0].package_id, "Contoso.Tool");
8632 assert_eq!(pins[0].version, "1.2.*");
8633 assert_eq!(pins[0].source_id, "winget");
8634 assert_eq!(pins[0].pin_type, PinType::Gating);
8635
8636 let conn = Connection::open(&db_path)?;
8637 let stored_pin = conn.query_row(
8638 "SELECT type, version FROM pin WHERE package_id = ?1 AND source_id = ?2",
8639 rusqlite::params!["Contoso.Tool", "winget"],
8640 |row| Ok((row.get::<_, i64>(0)?, row.get::<_, String>(1)?)),
8641 )?;
8642 assert_eq!(stored_pin.0, 3);
8643 assert_eq!(stored_pin.1, "1.2.*");
8644
8645 Ok(())
8646 })();
8647
8648 let _ = fs::remove_dir_all(&app_root);
8649 result.expect("packaged pin schema round trip");
8650 }
8651
8652 #[test]
8653 fn version_pin_patterns_match_exact_and_prefix_values() {
8654 assert!(version_matches_pin_pattern("1.2.3", "*"));
8655 assert!(version_matches_pin_pattern("1.2.3", "1.2.3"));
8656 assert!(version_matches_pin_pattern("1.2.3", "1.2.*"));
8657 assert!(!version_matches_pin_pattern("1.3.0", "1.2.*"));
8658 assert!(!version_matches_pin_pattern("1.2.3", "2.0.0"));
8659 }
8660
8661 #[test]
8662 fn source_specific_pins_override_source_agnostic_pins() {
8663 let item = ListMatch {
8664 name: "Contoso Tool".to_owned(),
8665 id: "Contoso.Tool".to_owned(),
8666 local_id: r"ARP\User\X64\Contoso.Tool".to_owned(),
8667 installed_version: "1.0.0".to_owned(),
8668 available_version: Some("2.0.0".to_owned()),
8669 source_name: Some("winget".to_owned()),
8670 publisher: None,
8671 scope: None,
8672 installer_category: None,
8673 install_location: None,
8674 package_family_names: Vec::new(),
8675 product_codes: Vec::new(),
8676 upgrade_codes: Vec::new(),
8677 };
8678 let pins = vec![
8679 PinRecord {
8680 package_id: "Contoso.Tool".to_owned(),
8681 version: "*".to_owned(),
8682 source_id: String::new(),
8683 pin_type: PinType::Blocking,
8684 },
8685 PinRecord {
8686 package_id: "Contoso.Tool".to_owned(),
8687 version: "2.0.*".to_owned(),
8688 source_id: "winget".to_owned(),
8689 pin_type: PinType::Pinning,
8690 },
8691 ];
8692
8693 assert!(!is_upgrade_blocked_by_pin(&item, &pins));
8694 }
8695
8696 #[test]
8697 fn blocking_pin_blocks_upgrade() {
8698 let item = ListMatch {
8699 name: "Contoso Tool".to_owned(),
8700 id: "Contoso.Tool".to_owned(),
8701 local_id: r"ARP\User\X64\Contoso.Tool".to_owned(),
8702 installed_version: "1.0.0".to_owned(),
8703 available_version: Some("2.0.0".to_owned()),
8704 source_name: Some("winget".to_owned()),
8705 publisher: None,
8706 scope: None,
8707 installer_category: None,
8708 install_location: None,
8709 package_family_names: Vec::new(),
8710 product_codes: Vec::new(),
8711 upgrade_codes: Vec::new(),
8712 };
8713 let pins = vec![PinRecord {
8714 package_id: "Contoso.Tool".to_owned(),
8715 version: "*".to_owned(),
8716 source_id: "winget".to_owned(),
8717 pin_type: PinType::Blocking,
8718 }];
8719
8720 assert!(is_upgrade_blocked_by_pin(&item, &pins));
8721 }
8722
8723 #[cfg(windows)]
8724 #[test]
8725 fn parses_msix_package_full_name_into_version_and_family() {
8726 let parsed = parse_msix_package_full_name("Microsoft.PowerToys.SparseApp_0.98.1.0_neutral__8wekyb3d8bbwe")
8727 .expect("package metadata");
8728
8729 assert_eq!(parsed.version, "0.98.1.0");
8730 assert_eq!(parsed.family_name, "Microsoft.PowerToys.SparseApp_8wekyb3d8bbwe");
8731 }
8732
8733 #[cfg(windows)]
8734 #[test]
8735 fn recognizes_windows_system_paths() {
8736 assert!(is_windows_system_path(r"C:\Windows\SystemApps\Contoso"));
8737 assert!(!is_windows_system_path(
8738 r"C:\Users\mamoreau\AppData\Local\PowerToys\WinUI3Apps"
8739 ));
8740 }
8741
8742 #[test]
8743 fn search_ranking_prefers_exact_name_over_tag_match() {
8744 let query = PackageQuery {
8745 query: Some("PowerToys".to_owned()),
8746 ..Default::default()
8747 };
8748 let exact_name = SearchMatch {
8749 source_name: "winget".to_owned(),
8750 source_kind: SourceKind::PreIndexed,
8751 id: "Microsoft.PowerToys".to_owned(),
8752 name: "PowerToys".to_owned(),
8753 moniker: Some("powertoys".to_owned()),
8754 version: Some("0.98.1".to_owned()),
8755 channel: None,
8756 match_criteria: Some("Moniker: powertoys".to_owned()),
8757 };
8758 let tag_match = SearchMatch {
8759 source_name: "winget".to_owned(),
8760 source_kind: SourceKind::PreIndexed,
8761 id: "JiriPolasek.QRCodesforCommandPalette".to_owned(),
8762 name: "QR Codes for Command Palette".to_owned(),
8763 moniker: None,
8764 version: Some("0.4.0.0".to_owned()),
8765 channel: None,
8766 match_criteria: Some("Tag: microsoft-powertoys".to_owned()),
8767 };
8768
8769 assert!(search_match_sort_score(&exact_name, &query) > search_match_sort_score(&tag_match, &query));
8770 }
8771
8772 #[test]
8773 fn select_best_text_match_prefers_exact_tag_value() {
8774 let values = vec![
8775 "microsoft-powertoys".to_owned(),
8776 "powertoys-run".to_owned(),
8777 "powertoys".to_owned(),
8778 ];
8779
8780 assert_eq!(
8781 select_best_text_match(values, "PowerToys", false).as_deref(),
8782 Some("powertoys")
8783 );
8784 }
8785
8786 #[test]
8787 fn search_match_criteria_is_blank_for_name_match() {
8788 let query = PackageQuery {
8789 query: Some("PowerToys".to_owned()),
8790 ..Default::default()
8791 };
8792
8793 let result = infer_match_criteria(
8794 "Microsoft.PowerToys",
8795 "PowerToys",
8796 Some("powertoys"),
8797 &query,
8798 SearchSemantics::Many,
8799 |_| Ok(Some("powertoys".to_owned())),
8800 |_| Ok(None),
8801 )
8802 .expect("match criteria");
8803
8804 assert_eq!(result, None);
8805 }
8806
8807 #[test]
8808 fn search_ranking_prefers_direct_name_match_even_with_unknown_version() {
8809 let query = PackageQuery {
8810 query: Some("PowerToys".to_owned()),
8811 ..Default::default()
8812 };
8813 let unknown_version = SearchMatch {
8814 source_name: "msstore".to_owned(),
8815 source_kind: SourceKind::Rest,
8816 id: "XP89DCGQ3K6VLD".to_owned(),
8817 name: "Microsoft PowerToys".to_owned(),
8818 moniker: None,
8819 version: Some("Unknown".to_owned()),
8820 channel: None,
8821 match_criteria: None,
8822 };
8823 let tag_match = SearchMatch {
8824 source_name: "winget".to_owned(),
8825 source_kind: SourceKind::PreIndexed,
8826 id: "Riri.QRCodeforCmdPal".to_owned(),
8827 name: "QR Code for CmdPal".to_owned(),
8828 moniker: None,
8829 version: Some("0.0.1.0".to_owned()),
8830 channel: None,
8831 match_criteria: Some("Tag: powertoys".to_owned()),
8832 };
8833
8834 assert!(search_match_sort_score(&unknown_version, &query) > search_match_sort_score(&tag_match, &query));
8835 }
8836
8837 #[test]
8838 fn search_source_fetch_results_ignore_small_display_count() {
8839 let query = PackageQuery {
8840 count: Some(5),
8841 ..Default::default()
8842 };
8843
8844 assert_eq!(source_fetch_results(&query, SearchSemantics::Many), 50);
8845 assert_eq!(source_fetch_results(&query, SearchSemantics::Single), 5);
8846 }
8847
8848 #[test]
8849 fn search_ranking_prefers_direct_name_prefix_over_tag_only_match() {
8850 let query = PackageQuery {
8851 query: Some("PowerToys".to_owned()),
8852 ..Default::default()
8853 };
8854 let prefix_name = SearchMatch {
8855 source_name: "winget".to_owned(),
8856 source_kind: SourceKind::PreIndexed,
8857 id: "advaith.CurrencyConverterPowerToys".to_owned(),
8858 name: "PowerToys-Run-Currency-Converter".to_owned(),
8859 moniker: None,
8860 version: Some("1.5.4".to_owned()),
8861 channel: None,
8862 match_criteria: None,
8863 };
8864 let exact_tag = SearchMatch {
8865 source_name: "winget".to_owned(),
8866 source_kind: SourceKind::PreIndexed,
8867 id: "Riri.QRCodeforCmdPal".to_owned(),
8868 name: "QR Code for CmdPal".to_owned(),
8869 moniker: None,
8870 version: Some("0.0.1.0".to_owned()),
8871 channel: None,
8872 match_criteria: Some("Tag: powertoys".to_owned()),
8873 };
8874
8875 assert!(search_match_sort_score(&prefix_name, &query) > search_match_sort_score(&exact_tag, &query));
8876 }
8877
8878 #[test]
8879 fn list_sort_prefers_main_package_then_sparse_app() {
8880 let main = InstalledPackage {
8881 name: "PowerToys (Preview) x64".to_owned(),
8882 local_id: r"ARP\User\X64\PowerToys".to_owned(),
8883 installed_version: "0.98.1".to_owned(),
8884 publisher: None,
8885 scope: Some("User".to_owned()),
8886 installer_category: Some("exe".to_owned()),
8887 install_location: None,
8888 package_family_names: Vec::new(),
8889 product_codes: Vec::new(),
8890 upgrade_codes: Vec::new(),
8891 correlated: None,
8892 };
8893 let sparse = InstalledPackage {
8894 name: "PowerToys.SparseApp".to_owned(),
8895 local_id: r"MSIX\Microsoft.PowerToys.SparseApp_0.98.1.0_neutral__8wekyb3d8bbwe".to_owned(),
8896 installed_version: "0.98.1.0".to_owned(),
8897 publisher: None,
8898 scope: Some("User".to_owned()),
8899 installer_category: Some("msix".to_owned()),
8900 install_location: None,
8901 package_family_names: Vec::new(),
8902 product_codes: Vec::new(),
8903 upgrade_codes: Vec::new(),
8904 correlated: None,
8905 };
8906 let extension = InstalledPackage {
8907 name: "PowerToys FileLocksmith Context Menu".to_owned(),
8908 local_id: r"MSIX\Microsoft.PowerToys.FileLocksmithContextMenu_0.98.1.0_neutral__8wekyb3d8bbwe".to_owned(),
8909 installed_version: "0.98.1.0".to_owned(),
8910 publisher: None,
8911 scope: Some("User".to_owned()),
8912 installer_category: Some("msix".to_owned()),
8913 install_location: None,
8914 package_family_names: Vec::new(),
8915 product_codes: Vec::new(),
8916 upgrade_codes: Vec::new(),
8917 correlated: None,
8918 };
8919
8920 assert!(list_sort_weight(&main) < list_sort_weight(&sparse));
8921 assert!(list_sort_weight(&sparse) < list_sort_weight(&extension));
8922 }
8923
8924 #[test]
8925 #[cfg(windows)]
8926 fn arp_subkey_name_extracts_registry_subkey() {
8927 assert_eq!(Some("ShareX"), arp_subkey_name(r"ARP\Machine\X64\ShareX"));
8928 assert_eq!(None, arp_subkey_name(r"MSIX\ShareX_19.0.2_x64__name"));
8929 }
8930
8931 #[test]
8932 #[cfg(windows)]
8933 fn registry_entry_matching_prefers_local_identity_over_correlated_id() {
8934 let installed = ListMatch {
8935 name: "ShareX".to_owned(),
8936 id: "ShareX.ShareX".to_owned(),
8937 local_id: r"ARP\Machine\X64\ShareX".to_owned(),
8938 installed_version: "19.0.2".to_owned(),
8939 available_version: None,
8940 source_name: Some("winget".to_owned()),
8941 publisher: Some("ShareX Team".to_owned()),
8942 scope: Some("Machine".to_owned()),
8943 installer_category: Some("exe".to_owned()),
8944 install_location: Some(r"C:\Program Files\ShareX".to_owned()),
8945 package_family_names: Vec::new(),
8946 product_codes: Vec::new(),
8947 upgrade_codes: Vec::new(),
8948 };
8949
8950 assert!(registry_entry_matches_installed_package(
8951 "ShareX", "ShareX", None, &installed
8952 ));
8953 assert!(!registry_entry_matches_installed_package(
8954 "ShareX.ShareX",
8955 "ShareX.ShareX",
8956 None,
8957 &installed
8958 ));
8959 }
8960
8961 #[test]
8962 #[cfg(windows)]
8963 fn build_uninstall_command_only_appends_silent_flag_when_needed() {
8964 assert_eq!(
8965 r#""C:\Program Files\ShareX\unins000.exe" /S"#,
8966 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe""#, true, false)
8967 );
8968 assert_eq!(
8969 r#""C:\Program Files\ShareX\unins000.exe" /VERYSILENT"#,
8970 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe" /VERYSILENT"#, true, false)
8971 );
8972 assert_eq!(
8973 r#""C:\Program Files\ShareX\unins000.exe""#,
8974 build_uninstall_command(r#""C:\Program Files\ShareX\unins000.exe""#, false, false)
8975 );
8976 assert_eq!(
8977 "winget uninstall --product-code JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe",
8978 build_uninstall_command(
8979 "winget uninstall --product-code JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe",
8980 true,
8981 false,
8982 )
8983 );
8984 }
8985
8986 #[test]
8987 #[cfg(windows)]
8988 fn build_winget_uninstall_arguments_prefers_correlated_identity() {
8989 let installed = ListMatch {
8990 name: "lazygit".to_owned(),
8991 id: "JesseDuffield.lazygit".to_owned(),
8992 local_id: r"ARP\User\X64\JesseDuffield.lazygit".to_owned(),
8993 installed_version: "0.61.1".to_owned(),
8994 available_version: None,
8995 source_name: Some("winget".to_owned()),
8996 publisher: Some("Jesse Duffield".to_owned()),
8997 scope: Some("User".to_owned()),
8998 installer_category: Some("exe".to_owned()),
8999 install_location: Some(
9000 r"C:\Users\test\AppData\Local\Microsoft\WinGet\Packages\JesseDuffield.lazygit".to_owned(),
9001 ),
9002 package_family_names: Vec::new(),
9003 product_codes: vec!["JesseDuffield.lazygit_Microsoft.Winget.Source_8wekyb3d8bbwe".to_owned()],
9004 upgrade_codes: Vec::new(),
9005 };
9006 let mut request = UninstallRequest::new(PackageQuery {
9007 id: Some("JesseDuffield.lazygit".to_owned()),
9008 install_scope: Some("user".to_owned()),
9009 ..PackageQuery::default()
9010 });
9011 request.mode = InstallerMode::Silent;
9012
9013 assert_eq!(
9014 Some(vec![
9015 "uninstall".to_owned(),
9016 "--id".to_owned(),
9017 "JesseDuffield.lazygit".to_owned(),
9018 "--exact".to_owned(),
9019 "--disable-interactivity".to_owned(),
9020 "--source".to_owned(),
9021 "winget".to_owned(),
9022 "--scope".to_owned(),
9023 "user".to_owned(),
9024 "--silent".to_owned(),
9025 ]),
9026 build_winget_uninstall_arguments(&installed, &request)
9027 );
9028
9029 assert_eq!(
9030 Some(vec![
9031 "uninstall".to_owned(),
9032 "--id".to_owned(),
9033 "JesseDuffield.lazygit".to_owned(),
9034 "--exact".to_owned(),
9035 "--disable-interactivity".to_owned(),
9036 "--source".to_owned(),
9037 "winget".to_owned(),
9038 "--silent".to_owned(),
9039 ]),
9040 build_winget_uninstall_arguments_with_scope(&installed, &request, false)
9041 );
9042 }
9043}