1use std::collections::BTreeMap;
78use std::fmt;
79
80use rucc_tuple::{Arch, TargetTuple};
81
82use crate::json::{JsonError, Reader};
83
84#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Payload {
87 pub name: String,
89 pub url: String,
91 pub sha256: String,
93 pub size: u64,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
103pub enum Chip {
104 X86,
106 X64,
108 Arm,
110 Arm64,
112}
113
114impl Chip {
115 #[must_use]
121 pub const fn of(target: TargetTuple) -> Option<Self> {
122 match target.arch() {
123 Arch::X86 => Some(Chip::X86),
124 Arch::X86_64 => Some(Chip::X64),
125 Arch::Arm => Some(Chip::Arm),
126 Arch::Aarch64 => Some(Chip::Arm64),
127 _ => None,
128 }
129 }
130
131 #[must_use]
133 pub const fn in_package(self) -> &'static str {
134 match self {
135 Chip::X86 => "x86",
136 Chip::X64 => "x64",
137 Chip::Arm => "arm",
138 Chip::Arm64 => "ARM64",
140 }
141 }
142
143 #[must_use]
145 pub const fn in_installer(self) -> &'static str {
146 match self {
147 Chip::X86 => "x86",
148 Chip::X64 => "x64",
149 Chip::Arm => "arm",
150 Chip::Arm64 => "arm64",
151 }
152 }
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct Channel {
158 pub release: String,
160 pub build: String,
162 pub manifest: Payload,
164 pub licence: String,
168}
169
170impl Channel {
171 pub fn parse(text: &str) -> Result<Self, MsvcError> {
178 let mut release = String::new();
179 let mut build = String::new();
180 let mut manifest = None;
181 let mut licence = String::new();
182
183 let mut reader = Reader::new(text);
184 reader.enter_object()?;
185 while let Some(key) = reader.next_key()? {
186 match &*key {
187 "info" => {
188 reader.enter_object()?;
189 while let Some(field) = reader.next_key()? {
190 match &*field {
191 "productDisplayVersion" => release = reader.string()?.into_owned(),
192 "buildVersion" => build = reader.string()?.into_owned(),
193 _ => reader.skip()?,
194 }
195 }
196 }
197 "channelItems" => {
198 reader.enter_array()?;
199 while reader.next_item()? {
200 let item = channel_item(&mut reader)?;
201 if item.kind == "Manifest" {
202 manifest = item.payload;
203 } else if item.id == BUILD_TOOLS && !item.licence.is_empty() {
204 licence = item.licence;
205 }
206 }
207 }
208 _ => reader.skip()?,
209 }
210 }
211
212 let manifest = manifest.ok_or(MsvcError::NoManifest)?;
213 if licence.is_empty() {
214 return Err(MsvcError::NoLicence);
215 }
216 Ok(Channel { release, build, manifest, licence })
217 }
218}
219
220const BUILD_TOOLS: &str = "Microsoft.VisualStudio.Product.BuildTools";
223
224struct ChannelItem {
226 id: String,
227 kind: String,
228 payload: Option<Payload>,
229 licence: String,
230}
231
232fn channel_item(reader: &mut Reader<'_>) -> Result<ChannelItem, MsvcError> {
234 let mut item = ChannelItem {
235 id: String::new(),
236 kind: String::new(),
237 payload: None,
238 licence: String::new(),
239 };
240 reader.enter_object()?;
241 while let Some(field) = reader.next_key()? {
242 match &*field {
243 "id" => item.id = reader.string()?.into_owned(),
244 "type" => item.kind = reader.string()?.into_owned(),
245 "payloads" => {
246 let mut all = payloads(reader)?;
247 item.payload = (!all.is_empty()).then(|| all.remove(0));
248 }
249 "localizedResources" => {
252 reader.enter_array()?;
253 while reader.next_item()? {
254 reader.enter_object()?;
255 while let Some(inner) = reader.next_key()? {
256 if inner == "license" && item.licence.is_empty() {
257 item.licence = reader.string()?.into_owned();
258 } else {
259 reader.skip()?;
260 }
261 }
262 }
263 }
264 _ => reader.skip()?,
265 }
266 }
267 Ok(item)
268}
269
270fn payloads(reader: &mut Reader<'_>) -> Result<Vec<Payload>, MsvcError> {
272 let mut all = Vec::new();
273 reader.enter_array()?;
274 while reader.next_item()? {
275 let mut one =
276 Payload { name: String::new(), url: String::new(), sha256: String::new(), size: 0 };
277 reader.enter_object()?;
278 while let Some(field) = reader.next_key()? {
279 match &*field {
280 "fileName" => one.name = reader.string()?.into_owned(),
281 "url" => one.url = reader.string()?.into_owned(),
282 "sha256" => one.sha256 = reader.string()?.to_ascii_lowercase(),
285 "size" => one.size = reader.integer()?,
286 _ => reader.skip()?,
287 }
288 }
289 all.push(one);
290 }
291 Ok(all)
292}
293
294#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct Wanted {
297 pub package: String,
299 pub version: String,
301 pub payload: Payload,
303}
304
305#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct Selection {
308 pub crt: String,
310 pub sdk: String,
312 pub files: Vec<Wanted>,
314 pub cabs: Vec<Wanted>,
321}
322
323impl Selection {
324 pub fn parse(text: &str, chips: &[Chip]) -> Result<Self, MsvcError> {
334 let found = collect(text)?;
335 let crt = newest(found.keys().filter_map(|id| vc_release(id)))
336 .ok_or(MsvcError::NothingFound("a Visual C++ CRT"))?;
337 let sdk_id = newest_sdk(&found).ok_or(MsvcError::NothingFound("a Windows SDK"))?;
338
339 let mut files = Vec::new();
340 let headers = format!("Microsoft.VC.{crt}.CRT.Headers.base");
341 let mut wanted = vec![headers.clone()];
342 for chip in sorted(chips) {
343 wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Desktop.base", chip.in_package()));
346 wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Store.base", chip.in_package()));
347 }
348 let mut crt_version = String::new();
351 for id in wanted {
352 let package = found.get(&id).ok_or_else(|| MsvcError::NoPackage(id.clone()))?;
353 if id == headers {
354 crt_version.clone_from(&package.version);
355 }
356 for payload in &package.payloads {
357 files.push(Wanted {
358 package: id.clone(),
359 version: package.version.clone(),
360 payload: payload.clone(),
361 });
362 }
363 }
364
365 let sdk = found.get(&sdk_id).ok_or_else(|| MsvcError::NoPackage(sdk_id.clone()))?;
366 for installer in installers(chips) {
367 let payload = sdk
368 .payloads
369 .iter()
370 .find(|payload| leaf(&payload.name) == installer)
371 .ok_or_else(|| MsvcError::NoInstaller(installer.clone()))?;
372 files.push(Wanted {
373 package: sdk_id.clone(),
374 version: sdk.version.clone(),
375 payload: payload.clone(),
376 });
377 }
378
379 let mut cabs: Vec<Wanted> = sdk
380 .payloads
381 .iter()
382 .filter(|payload| leaf(&payload.name).to_ascii_lowercase().ends_with(".cab"))
383 .map(|payload| Wanted {
384 package: sdk_id.clone(),
385 version: sdk.version.clone(),
386 payload: payload.clone(),
387 })
388 .collect();
389 cabs.sort_by(|a, b| a.payload.name.cmp(&b.payload.name));
390
391 files.sort_by(|a, b| (&a.package, &a.payload.name).cmp(&(&b.package, &b.payload.name)));
392 Ok(Selection { crt: crt_version, sdk: sdk.version.clone(), files, cabs })
393 }
394
395 #[must_use]
403 pub fn cab(&self, name: &str) -> Option<&Wanted> {
404 self.cabs.iter().find(|cab| leaf(&cab.payload.name).eq_ignore_ascii_case(leaf(name)))
405 }
406
407 #[must_use]
415 pub fn size(&self) -> u64 {
416 self.files.iter().map(|file| file.payload.size).sum()
417 }
418}
419
420fn installers(chips: &[Chip]) -> Vec<String> {
428 let mut all = vec![
429 "Universal CRT Headers Libraries and Sources-x86_en-us.msi".to_owned(),
430 "Windows SDK Desktop Headers x86-x86_en-us.msi".to_owned(),
431 "Windows SDK OnecoreUap Headers x86-x86_en-us.msi".to_owned(),
432 "Windows SDK for Windows Store Apps Headers-x86_en-us.msi".to_owned(),
433 "Windows SDK for Windows Store Apps Libs-x86_en-us.msi".to_owned(),
434 ];
435 for chip in sorted(chips) {
436 let arch = chip.in_installer();
437 all.push(format!("Windows SDK Desktop Headers {arch}-x86_en-us.msi"));
438 all.push(format!("Windows SDK Desktop Libs {arch}-x86_en-us.msi"));
439 }
440 all.sort();
441 all.dedup();
442 all
443}
444
445fn sorted(chips: &[Chip]) -> Vec<Chip> {
448 let mut all = chips.to_vec();
449 all.sort_unstable();
450 all.dedup();
451 all
452}
453
454#[derive(Debug)]
456struct Package {
457 version: String,
458 payloads: Vec<Payload>,
459}
460
461fn collect(text: &str) -> Result<BTreeMap<String, Package>, MsvcError> {
468 let mut found: BTreeMap<String, Package> = BTreeMap::new();
469 let mut reader = Reader::new(text);
470 reader.enter_object()?;
471 while let Some(key) = reader.next_key()? {
472 if key != "packages" {
473 reader.skip()?;
474 continue;
475 }
476 reader.enter_array()?;
477 while reader.next_item()? {
478 let mut id = String::new();
479 let mut version = String::new();
480 let mut keep = None;
481 let mut listed = false;
482 let mut read = false;
483 reader.enter_object()?;
484 while let Some(field) = reader.next_key()? {
485 match &*field {
486 "id" => {
487 id = reader.string()?.into_owned();
488 keep = Some(interesting(&id));
489 }
490 "version" => version = reader.string()?.into_owned(),
491 "payloads" => {
492 listed = true;
493 read = keep == Some(true);
494 if read {
495 let all = payloads(&mut reader)?;
496 found
497 .entry(id.clone())
498 .or_insert(Package { version: version.clone(), payloads: all });
499 } else {
500 reader.skip()?;
501 }
502 }
503 _ => reader.skip()?,
504 }
505 }
506 if keep == Some(true) && listed && !read && !found.contains_key(&id) {
510 return Err(MsvcError::OutOfOrder(id));
511 }
512 if let Some(package) = found.get_mut(&id) {
515 if package.version.is_empty() {
516 package.version.clone_from(&version);
517 }
518 }
519 }
520 }
521 Ok(found)
522}
523
524fn interesting(id: &str) -> bool {
529 (id.starts_with("Microsoft.VC.") && id.contains(".CRT.") && id.ends_with(".base"))
530 || id.starts_with("Win10SDK_10.0.")
531 || id.starts_with("Win11SDK_10.0.")
532}
533
534fn vc_release(id: &str) -> Option<&str> {
540 let rest = id.strip_prefix("Microsoft.VC.")?;
541 let at = rest.find(".CRT.")?;
542 let release = &rest[..at];
543 release
544 .split('.')
545 .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
546 .then_some(release)
547}
548
549fn newest<'a>(all: impl Iterator<Item = &'a str>) -> Option<String> {
554 all.max_by(|left, right| numbers(left).cmp(&numbers(right))).map(ToOwned::to_owned)
555}
556
557fn numbers(text: &str) -> Vec<u64> {
559 text.split('.').map(|part| part.parse().unwrap_or(0)).collect()
560}
561
562fn newest_sdk(found: &BTreeMap<String, Package>) -> Option<String> {
568 found
569 .iter()
570 .filter(|(id, _)| id.starts_with("Win10SDK_10.0.") || id.starts_with("Win11SDK_10.0."))
571 .max_by_key(|(id, package)| {
572 let build = id.rsplit('.').next().and_then(|last| last.parse::<u64>().ok());
573 (build.unwrap_or(0), numbers(&package.version))
574 })
575 .map(|(id, _)| id.clone())
576}
577
578fn leaf(name: &str) -> &str {
580 name.rsplit('\\').next().unwrap_or(name)
581}
582
583#[derive(Debug, Clone, PartialEq, Eq)]
585pub enum MsvcError {
586 Json {
591 at: usize,
593 wanted: &'static str,
595 },
596 NoManifest,
598 NoLicence,
601 NothingFound(&'static str),
603 NoPackage(String),
605 NoInstaller(String),
607 OutOfOrder(String),
610}
611
612impl From<JsonError> for MsvcError {
613 fn from(why: JsonError) -> Self {
614 MsvcError::Json { at: why.at, wanted: why.wanted }
615 }
616}
617
618impl fmt::Display for MsvcError {
619 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
620 match self {
621 MsvcError::Json { at, wanted } => {
622 write!(f, "this is not the document it was taken for: {wanted} at byte {at} of it")
623 }
624 MsvcError::NoManifest => {
625 write!(f, "this channel names no installer manifest, so it is not a channel")
626 }
627 MsvcError::NoLicence => write!(
628 f,
629 "this channel names no licence for the build tools, and the download it describes \
630 is one nobody may make without reading one"
631 ),
632 MsvcError::NothingFound(what) => {
633 write!(f, "this manifest has no {what} in it")
634 }
635 MsvcError::NoPackage(id) => {
636 write!(
637 f,
638 "this manifest has no {id}, which is a package an MSVC sysroot is made of"
639 )
640 }
641 MsvcError::NoInstaller(name) => write!(
642 f,
643 "the Windows SDK in this manifest has no {name} in it, which is an installer an \
644 MSVC sysroot is made of"
645 ),
646 MsvcError::OutOfOrder(id) => write!(
647 f,
648 "{id} lists its files before it says what it is, which this reader relies on the \
649 manifest not doing"
650 ),
651 }
652 }
653}
654
655impl std::error::Error for MsvcError {}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 const CHANNEL: &str = r#"{
664 "manifestVersion": "1.1",
665 "info": { "buildVersion": "17.14.37710.0", "productDisplayVersion": "17.14.41 (September 2026)" },
666 "channelItems": [
667 {
668 "id": "Microsoft.VisualStudio.Manifests.VisualStudio",
669 "version": "17.14.37710.0",
670 "type": "Manifest",
671 "payloads": [
672 {
673 "fileName": "VisualStudio.vsman",
674 "sha256": "6E470016E4324C84C255FFD0BEB3767D17EC89CC8561E9409EE3E1F6D29400F5",
675 "size": 30443537,
676 "url": "https://download.visualstudio.microsoft.com/download/pr/bc92e2cb/VisualStudio.vsman"
677 }
678 ]
679 },
680 {
681 "id": "Microsoft.VisualStudio.Product.BuildTools",
682 "type": "ChannelProduct",
683 "localizedResources": [
684 { "language": "en-US", "license": "https://go.microsoft.com/fwlink/?LinkId=2179911" }
685 ]
686 }
687 ]
688 }"#;
689
690 const MANIFEST: &str = r#"{
694 "manifestVersion": "1.1",
695 "packages": [
696 { "id": "Microsoft.VC.14.29.16.11.CRT.Headers.base", "version": "14.29.30157", "type": "Vsix",
697 "payloads": [ { "fileName": "old.vsix", "sha256": "aa", "size": 1, "url": "https://example.invalid/old" } ] },
698 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220", "type": "Vsix",
699 "payloads": [ { "fileName": "headers.vsix", "sha256": "B1", "size": 2128977, "url": "https://example.invalid/headers" } ] },
700 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.Resources", "language": "de-DE", "version": "14.44.35220", "type": "Vsix",
701 "payloads": [ { "fileName": "de.vsix", "sha256": "cc", "size": 3, "url": "https://example.invalid/de" } ] },
702 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
703 "payloads": [ { "fileName": "x64.vsix", "sha256": "b2", "size": 51521199, "url": "https://example.invalid/x64" } ] },
704 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Store.base", "version": "14.44.35226", "type": "Vsix",
705 "payloads": [ { "fileName": "x64-store.vsix", "sha256": "b4", "size": 28032384, "url": "https://example.invalid/x64-store" } ] },
706 { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
707 "payloads": [ { "fileName": "arm64.vsix", "sha256": "b3", "size": 49166761, "url": "https://example.invalid/arm64" } ] },
708 { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Store.base", "version": "14.44.35226", "type": "Vsix",
709 "payloads": [ { "fileName": "arm64-store.vsix", "sha256": "b5", "size": 26214400, "url": "https://example.invalid/arm64-store" } ] },
710 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base", "version": "14.44.35226", "type": "Vsix",
711 "payloads": [ { "fileName": "spectre.vsix", "sha256": "dd", "size": 4, "url": "https://example.invalid/spectre" } ] },
712 { "id": "Microsoft.VisualStudio.Component.Windows11SDK", "version": "17.14.35", "type": "Component",
713 "payloads": [ { "fileName": "nothing.vsix", "sha256": "ee", "size": 5, "url": "https://example.invalid/nothing" } ] },
714 { "id": "Win10SDK_10.0.19041", "version": "10.0.19041.4", "type": "Exe",
715 "payloads": [ { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "ff", "size": 6, "url": "https://example.invalid/old-sdk" } ] },
716 { "id": "Win11SDK_10.0.26100", "version": "10.0.26100.15", "type": "Exe",
717 "payloads": [
718 { "fileName": "Installers\\Universal CRT Headers Libraries and Sources-x86_en-us.msi", "sha256": "c1", "size": 589824, "url": "https://example.invalid/ucrt" },
719 { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "c2", "size": 790528, "url": "https://example.invalid/hx86" },
720 { "fileName": "Installers\\Windows SDK Desktop Headers x64-x86_en-us.msi", "sha256": "c3", "size": 450560, "url": "https://example.invalid/hx64" },
721 { "fileName": "Installers\\Windows SDK Desktop Headers arm64-x86_en-us.msi", "sha256": "c4", "size": 446464, "url": "https://example.invalid/harm64" },
722 { "fileName": "Installers\\Windows SDK Desktop Libs x86-x86_en-us.msi", "sha256": "c5", "size": 528384, "url": "https://example.invalid/lx86" },
723 { "fileName": "Installers\\Windows SDK Desktop Libs x64-x86_en-us.msi", "sha256": "c6", "size": 528384, "url": "https://example.invalid/lx64" },
724 { "fileName": "Installers\\Windows SDK Desktop Libs arm64-x86_en-us.msi", "sha256": "c7", "size": 528384, "url": "https://example.invalid/larm64" },
725 { "fileName": "Installers\\Windows SDK OnecoreUap Headers x86-x86_en-us.msi", "sha256": "c8", "size": 495616, "url": "https://example.invalid/onecore" },
726 { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers-x86_en-us.msi", "sha256": "c9", "size": 1060864, "url": "https://example.invalid/store-h" },
727 { "fileName": "Installers\\Windows SDK for Windows Store Apps Libs-x86_en-us.msi", "sha256": "ca", "size": 528384, "url": "https://example.invalid/store-l" },
728 { "fileName": "Installers\\Windows SDK Desktop Tools x64-x86_en-us.msi", "sha256": "cb", "size": 475136, "url": "https://example.invalid/tools" },
729 { "fileName": "Installers\\0f1a2b3c.cab", "sha256": "cc", "size": 9999, "url": "https://example.invalid/cab" },
730 { "fileName": "Installers\\7e6d5c4b.cab", "sha256": "cd", "size": 8888, "url": "https://example.invalid/other-cab" }
731 ] }
732 ]
733 }"#;
734
735 fn target(tuple: &str) -> TargetTuple {
736 tuple.parse().expect("a target this understands")
737 }
738
739 #[test]
740 fn a_channel_says_the_release_the_manifest_and_where_the_licence_is() {
741 let channel = Channel::parse(CHANNEL).expect("a channel");
742 assert_eq!(channel.release, "17.14.41 (September 2026)");
743 assert_eq!(channel.build, "17.14.37710.0");
744 assert_eq!(channel.manifest.name, "VisualStudio.vsman");
745 assert_eq!(channel.manifest.size, 30_443_537);
746 assert!(channel.manifest.sha256.starts_with("6e470016"), "{}", channel.manifest.sha256);
748 assert_eq!(channel.licence, "https://go.microsoft.com/fwlink/?LinkId=2179911");
749 }
750
751 #[test]
752 fn a_channel_with_no_manifest_or_no_licence_in_it_says_which() {
753 let without = CHANNEL.replace("\"type\": \"Manifest\"", "\"type\": \"Bootstrapper\"");
754 assert_eq!(Channel::parse(&without).expect_err("no manifest"), MsvcError::NoManifest);
755
756 let without = CHANNEL.replace("Microsoft.VisualStudio.Product.BuildTools", "Other.Product");
757 assert_eq!(Channel::parse(&without).expect_err("no licence"), MsvcError::NoLicence);
758 }
759
760 #[test]
761 fn the_newest_visual_cpp_and_the_newest_kit_are_the_ones_chosen() {
762 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
763 assert_eq!(chosen.crt, "14.44.35220");
764 assert_eq!(chosen.sdk, "10.0.26100.15");
765 let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
766 assert!(!packages.contains(&"Microsoft.VC.14.29.16.11.CRT.Headers.base"), "{packages:?}");
767 assert!(!packages.contains(&"Win10SDK_10.0.19041"), "{packages:?}");
768 }
769
770 #[test]
771 fn a_selection_is_the_headers_two_library_packages_per_chip_and_the_installers() {
772 let one = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
773 assert_eq!(one.files.len(), 1 + 2 + 7);
774
775 let two = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
776 assert_eq!(two.files.len(), one.files.len() + 4);
778 assert!(two.size() > one.size());
779
780 let again =
782 Selection::parse(MANIFEST, &[Chip::Arm64, Chip::X64, Chip::X64]).expect("a selection");
783 assert_eq!(again, two);
784 }
785
786 #[test]
787 fn nothing_that_is_not_a_header_or_a_library_is_chosen() {
788 let chosen = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
789 let names: Vec<&str> = chosen.files.iter().map(|file| leaf(&file.payload.name)).collect();
790 for unwanted in ["spectre.vsix", "de.vsix", "nothing.vsix"] {
791 assert!(!names.contains(&unwanted), "{unwanted} is in {names:?}");
792 }
793 assert!(!names.iter().any(|name| name.contains("Tools")), "{names:?}");
796 assert!(!names.iter().any(|name| name.ends_with(".cab")), "{names:?}");
797 }
798
799 #[test]
800 fn the_store_package_is_taken_for_its_import_libraries_and_the_spectre_one_is_not() {
801 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
802 let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
803 assert!(packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Store.base"), "{packages:?}");
806 assert!(
807 packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base"),
808 "{packages:?}"
809 );
810 assert!(
811 !packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base"),
812 "{packages:?}"
813 );
814 }
815
816 #[test]
817 fn a_cabinet_is_found_by_the_name_an_installer_gives_it() {
818 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
819 assert_eq!(chosen.cabs.len(), 2);
820 let cab = chosen.cab("0f1a2b3c.cab").expect("the cabinet");
823 assert_eq!(cab.payload.name, r"Installers\0f1a2b3c.cab");
824 assert_eq!(cab.payload.url, "https://example.invalid/cab");
825 assert_eq!(cab.package, "Win11SDK_10.0.26100");
826 assert_eq!(chosen.cab("0F1A2B3C.CAB"), Some(cab));
828 assert_eq!(chosen.cab("nothing.cab"), None);
829 }
830
831 #[test]
832 fn a_missing_package_or_installer_says_which_one_by_name() {
833 let without = MANIFEST.replace("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "Other");
834 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
835 assert_eq!(
836 why,
837 MsvcError::NoPackage("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base".into())
838 );
839
840 let without =
841 MANIFEST.replace("Windows SDK Desktop Libs x64", "Windows SDK Desktop Libs mips");
842 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
843 assert_eq!(
844 why,
845 MsvcError::NoInstaller("Windows SDK Desktop Libs x64-x86_en-us.msi".into())
846 );
847
848 let empty = r#"{ "packages": [] }"#;
849 assert_eq!(
850 Selection::parse(empty, &[Chip::X64]).expect_err("a refusal"),
851 MsvcError::NothingFound("a Visual C++ CRT")
852 );
853 }
854
855 #[test]
856 fn the_windows_path_in_an_installer_name_survives_being_read() {
857 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
858 let ucrt = chosen
859 .files
860 .iter()
861 .find(|file| leaf(&file.payload.name).starts_with("Universal CRT"))
862 .expect("the universal CRT");
863 assert_eq!(
864 ucrt.payload.name,
865 r"Installers\Universal CRT Headers Libraries and Sources-x86_en-us.msi"
866 );
867 assert_eq!(ucrt.version, "10.0.26100.15");
868 }
869
870 #[test]
871 fn a_target_maps_to_the_chip_microsoft_spells_two_ways() {
872 assert_eq!(Chip::of(target("x86_64-windows-msvc")), Some(Chip::X64));
873 assert_eq!(Chip::of(target("aarch64-windows-msvc")), Some(Chip::Arm64));
874 assert_eq!(Chip::of(target("i686-windows-msvc")), Some(Chip::X86));
875 assert_eq!(Chip::of(target("arm64ec-windows-msvc")), None);
877 assert_eq!(Chip::of(target("riscv64-linux-gnu")), None);
878
879 assert_eq!(Chip::Arm64.in_package(), "ARM64");
880 assert_eq!(Chip::Arm64.in_installer(), "arm64");
881 }
882
883 #[test]
884 fn a_dotted_version_is_compared_as_numbers_and_not_as_text() {
885 assert_eq!(
888 newest(["10.0.9.0", "10.0.22000.0"].into_iter()).as_deref(),
889 Some("10.0.22000.0")
890 );
891 assert_eq!(vc_release("Microsoft.VC.14.44.17.14.CRT.Headers.base"), Some("14.44.17.14"));
892 assert_eq!(vc_release("Microsoft.VC.Runtimes.x64.base"), None);
893 }
894
895 #[test]
896 fn a_package_that_lists_its_files_before_it_says_what_it_is_is_refused() {
897 let backwards = r#"{ "packages": [
898 { "payloads": [ { "fileName": "a", "sha256": "b", "size": 1, "url": "c" } ],
899 "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220" } ] }"#;
900 let why = Selection::parse(backwards, &[Chip::X64]).expect_err("a refusal");
901 assert_eq!(
902 why,
903 MsvcError::OutOfOrder("Microsoft.VC.14.44.17.14.CRT.Headers.base".to_owned())
904 );
905 }
906}