1use std::collections::BTreeMap;
52use std::fmt;
53
54use rucc_tuple::{Arch, TargetTuple};
55
56use crate::json::{JsonError, Reader};
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Payload {
61 pub name: String,
63 pub url: String,
65 pub sha256: String,
67 pub size: u64,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
77pub enum Chip {
78 X86,
80 X64,
82 Arm,
84 Arm64,
86}
87
88impl Chip {
89 #[must_use]
95 pub const fn of(target: TargetTuple) -> Option<Self> {
96 match target.arch() {
97 Arch::X86 => Some(Chip::X86),
98 Arch::X86_64 => Some(Chip::X64),
99 Arch::Arm => Some(Chip::Arm),
100 Arch::Aarch64 => Some(Chip::Arm64),
101 _ => None,
102 }
103 }
104
105 #[must_use]
107 pub const fn in_package(self) -> &'static str {
108 match self {
109 Chip::X86 => "x86",
110 Chip::X64 => "x64",
111 Chip::Arm => "arm",
112 Chip::Arm64 => "ARM64",
114 }
115 }
116
117 #[must_use]
119 pub const fn in_installer(self) -> &'static str {
120 match self {
121 Chip::X86 => "x86",
122 Chip::X64 => "x64",
123 Chip::Arm => "arm",
124 Chip::Arm64 => "arm64",
125 }
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct Channel {
132 pub release: String,
134 pub build: String,
136 pub manifest: Payload,
138 pub licence: String,
142}
143
144impl Channel {
145 pub fn parse(text: &str) -> Result<Self, MsvcError> {
152 let mut release = String::new();
153 let mut build = String::new();
154 let mut manifest = None;
155 let mut licence = String::new();
156
157 let mut reader = Reader::new(text);
158 reader.enter_object()?;
159 while let Some(key) = reader.next_key()? {
160 match &*key {
161 "info" => {
162 reader.enter_object()?;
163 while let Some(field) = reader.next_key()? {
164 match &*field {
165 "productDisplayVersion" => release = reader.string()?.into_owned(),
166 "buildVersion" => build = reader.string()?.into_owned(),
167 _ => reader.skip()?,
168 }
169 }
170 }
171 "channelItems" => {
172 reader.enter_array()?;
173 while reader.next_item()? {
174 let item = channel_item(&mut reader)?;
175 if item.kind == "Manifest" {
176 manifest = item.payload;
177 } else if item.id == BUILD_TOOLS && !item.licence.is_empty() {
178 licence = item.licence;
179 }
180 }
181 }
182 _ => reader.skip()?,
183 }
184 }
185
186 let manifest = manifest.ok_or(MsvcError::NoManifest)?;
187 if licence.is_empty() {
188 return Err(MsvcError::NoLicence);
189 }
190 Ok(Channel { release, build, manifest, licence })
191 }
192}
193
194const BUILD_TOOLS: &str = "Microsoft.VisualStudio.Product.BuildTools";
197
198struct ChannelItem {
200 id: String,
201 kind: String,
202 payload: Option<Payload>,
203 licence: String,
204}
205
206fn channel_item(reader: &mut Reader<'_>) -> Result<ChannelItem, MsvcError> {
208 let mut item = ChannelItem {
209 id: String::new(),
210 kind: String::new(),
211 payload: None,
212 licence: String::new(),
213 };
214 reader.enter_object()?;
215 while let Some(field) = reader.next_key()? {
216 match &*field {
217 "id" => item.id = reader.string()?.into_owned(),
218 "type" => item.kind = reader.string()?.into_owned(),
219 "payloads" => {
220 let mut all = payloads(reader)?;
221 item.payload = (!all.is_empty()).then(|| all.remove(0));
222 }
223 "localizedResources" => {
226 reader.enter_array()?;
227 while reader.next_item()? {
228 reader.enter_object()?;
229 while let Some(inner) = reader.next_key()? {
230 if inner == "license" && item.licence.is_empty() {
231 item.licence = reader.string()?.into_owned();
232 } else {
233 reader.skip()?;
234 }
235 }
236 }
237 }
238 _ => reader.skip()?,
239 }
240 }
241 Ok(item)
242}
243
244fn payloads(reader: &mut Reader<'_>) -> Result<Vec<Payload>, MsvcError> {
246 let mut all = Vec::new();
247 reader.enter_array()?;
248 while reader.next_item()? {
249 let mut one =
250 Payload { name: String::new(), url: String::new(), sha256: String::new(), size: 0 };
251 reader.enter_object()?;
252 while let Some(field) = reader.next_key()? {
253 match &*field {
254 "fileName" => one.name = reader.string()?.into_owned(),
255 "url" => one.url = reader.string()?.into_owned(),
256 "sha256" => one.sha256 = reader.string()?.to_ascii_lowercase(),
259 "size" => one.size = reader.integer()?,
260 _ => reader.skip()?,
261 }
262 }
263 all.push(one);
264 }
265 Ok(all)
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Wanted {
271 pub package: String,
273 pub version: String,
275 pub payload: Payload,
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct Selection {
282 pub crt: String,
284 pub sdk: String,
286 pub files: Vec<Wanted>,
288}
289
290impl Selection {
291 pub fn parse(text: &str, chips: &[Chip]) -> Result<Self, MsvcError> {
301 let found = collect(text)?;
302 let crt = newest(found.keys().filter_map(|id| vc_release(id)))
303 .ok_or(MsvcError::NothingFound("a Visual C++ CRT"))?;
304 let sdk_id = newest_sdk(&found).ok_or(MsvcError::NothingFound("a Windows SDK"))?;
305
306 let mut files = Vec::new();
307 let headers = format!("Microsoft.VC.{crt}.CRT.Headers.base");
308 let mut wanted = vec![headers.clone()];
309 for chip in sorted(chips) {
310 wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Desktop.base", chip.in_package()));
311 }
312 let mut crt_version = String::new();
315 for id in wanted {
316 let package = found.get(&id).ok_or_else(|| MsvcError::NoPackage(id.clone()))?;
317 if id == headers {
318 crt_version.clone_from(&package.version);
319 }
320 for payload in &package.payloads {
321 files.push(Wanted {
322 package: id.clone(),
323 version: package.version.clone(),
324 payload: payload.clone(),
325 });
326 }
327 }
328
329 let sdk = found.get(&sdk_id).ok_or_else(|| MsvcError::NoPackage(sdk_id.clone()))?;
330 for installer in installers(chips) {
331 let payload = sdk
332 .payloads
333 .iter()
334 .find(|payload| leaf(&payload.name) == installer)
335 .ok_or_else(|| MsvcError::NoInstaller(installer.clone()))?;
336 files.push(Wanted {
337 package: sdk_id.clone(),
338 version: sdk.version.clone(),
339 payload: payload.clone(),
340 });
341 }
342
343 files.sort_by(|a, b| (&a.package, &a.payload.name).cmp(&(&b.package, &b.payload.name)));
344 Ok(Selection { crt: crt_version, sdk: sdk.version.clone(), files })
345 }
346
347 #[must_use]
349 pub fn size(&self) -> u64 {
350 self.files.iter().map(|file| file.payload.size).sum()
351 }
352}
353
354fn installers(chips: &[Chip]) -> Vec<String> {
362 let mut all = vec![
363 "Universal CRT Headers Libraries and Sources-x86_en-us.msi".to_owned(),
364 "Windows SDK Desktop Headers x86-x86_en-us.msi".to_owned(),
365 "Windows SDK OnecoreUap Headers x86-x86_en-us.msi".to_owned(),
366 "Windows SDK for Windows Store Apps Headers-x86_en-us.msi".to_owned(),
367 "Windows SDK for Windows Store Apps Libs-x86_en-us.msi".to_owned(),
368 ];
369 for chip in sorted(chips) {
370 let arch = chip.in_installer();
371 all.push(format!("Windows SDK Desktop Headers {arch}-x86_en-us.msi"));
372 all.push(format!("Windows SDK Desktop Libs {arch}-x86_en-us.msi"));
373 }
374 all.sort();
375 all.dedup();
376 all
377}
378
379fn sorted(chips: &[Chip]) -> Vec<Chip> {
382 let mut all = chips.to_vec();
383 all.sort_unstable();
384 all.dedup();
385 all
386}
387
388#[derive(Debug)]
390struct Package {
391 version: String,
392 payloads: Vec<Payload>,
393}
394
395fn collect(text: &str) -> Result<BTreeMap<String, Package>, MsvcError> {
402 let mut found: BTreeMap<String, Package> = BTreeMap::new();
403 let mut reader = Reader::new(text);
404 reader.enter_object()?;
405 while let Some(key) = reader.next_key()? {
406 if key != "packages" {
407 reader.skip()?;
408 continue;
409 }
410 reader.enter_array()?;
411 while reader.next_item()? {
412 let mut id = String::new();
413 let mut version = String::new();
414 let mut keep = None;
415 let mut listed = false;
416 let mut read = false;
417 reader.enter_object()?;
418 while let Some(field) = reader.next_key()? {
419 match &*field {
420 "id" => {
421 id = reader.string()?.into_owned();
422 keep = Some(interesting(&id));
423 }
424 "version" => version = reader.string()?.into_owned(),
425 "payloads" => {
426 listed = true;
427 read = keep == Some(true);
428 if read {
429 let all = payloads(&mut reader)?;
430 found
431 .entry(id.clone())
432 .or_insert(Package { version: version.clone(), payloads: all });
433 } else {
434 reader.skip()?;
435 }
436 }
437 _ => reader.skip()?,
438 }
439 }
440 if keep == Some(true) && listed && !read && !found.contains_key(&id) {
444 return Err(MsvcError::OutOfOrder(id));
445 }
446 if let Some(package) = found.get_mut(&id) {
449 if package.version.is_empty() {
450 package.version.clone_from(&version);
451 }
452 }
453 }
454 }
455 Ok(found)
456}
457
458fn interesting(id: &str) -> bool {
463 (id.starts_with("Microsoft.VC.") && id.contains(".CRT.") && id.ends_with(".base"))
464 || id.starts_with("Win10SDK_10.0.")
465 || id.starts_with("Win11SDK_10.0.")
466}
467
468fn vc_release(id: &str) -> Option<&str> {
474 let rest = id.strip_prefix("Microsoft.VC.")?;
475 let at = rest.find(".CRT.")?;
476 let release = &rest[..at];
477 release
478 .split('.')
479 .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
480 .then_some(release)
481}
482
483fn newest<'a>(all: impl Iterator<Item = &'a str>) -> Option<String> {
488 all.max_by(|left, right| numbers(left).cmp(&numbers(right))).map(ToOwned::to_owned)
489}
490
491fn numbers(text: &str) -> Vec<u64> {
493 text.split('.').map(|part| part.parse().unwrap_or(0)).collect()
494}
495
496fn newest_sdk(found: &BTreeMap<String, Package>) -> Option<String> {
502 found
503 .iter()
504 .filter(|(id, _)| id.starts_with("Win10SDK_10.0.") || id.starts_with("Win11SDK_10.0."))
505 .max_by_key(|(id, package)| {
506 let build = id.rsplit('.').next().and_then(|last| last.parse::<u64>().ok());
507 (build.unwrap_or(0), numbers(&package.version))
508 })
509 .map(|(id, _)| id.clone())
510}
511
512fn leaf(name: &str) -> &str {
514 name.rsplit('\\').next().unwrap_or(name)
515}
516
517#[derive(Debug, Clone, PartialEq, Eq)]
519pub enum MsvcError {
520 Json {
525 at: usize,
527 wanted: &'static str,
529 },
530 NoManifest,
532 NoLicence,
535 NothingFound(&'static str),
537 NoPackage(String),
539 NoInstaller(String),
541 OutOfOrder(String),
544}
545
546impl From<JsonError> for MsvcError {
547 fn from(why: JsonError) -> Self {
548 MsvcError::Json { at: why.at, wanted: why.wanted }
549 }
550}
551
552impl fmt::Display for MsvcError {
553 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554 match self {
555 MsvcError::Json { at, wanted } => {
556 write!(f, "this is not the document it was taken for: {wanted} at byte {at} of it")
557 }
558 MsvcError::NoManifest => {
559 write!(f, "this channel names no installer manifest, so it is not a channel")
560 }
561 MsvcError::NoLicence => write!(
562 f,
563 "this channel names no licence for the build tools, and the download it describes \
564 is one nobody may make without reading one"
565 ),
566 MsvcError::NothingFound(what) => {
567 write!(f, "this manifest has no {what} in it")
568 }
569 MsvcError::NoPackage(id) => {
570 write!(
571 f,
572 "this manifest has no {id}, which is a package an MSVC sysroot is made of"
573 )
574 }
575 MsvcError::NoInstaller(name) => write!(
576 f,
577 "the Windows SDK in this manifest has no {name} in it, which is an installer an \
578 MSVC sysroot is made of"
579 ),
580 MsvcError::OutOfOrder(id) => write!(
581 f,
582 "{id} lists its files before it says what it is, which this reader relies on the \
583 manifest not doing"
584 ),
585 }
586 }
587}
588
589impl std::error::Error for MsvcError {}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 const CHANNEL: &str = r#"{
598 "manifestVersion": "1.1",
599 "info": { "buildVersion": "17.14.37710.0", "productDisplayVersion": "17.14.41 (September 2026)" },
600 "channelItems": [
601 {
602 "id": "Microsoft.VisualStudio.Manifests.VisualStudio",
603 "version": "17.14.37710.0",
604 "type": "Manifest",
605 "payloads": [
606 {
607 "fileName": "VisualStudio.vsman",
608 "sha256": "6E470016E4324C84C255FFD0BEB3767D17EC89CC8561E9409EE3E1F6D29400F5",
609 "size": 30443537,
610 "url": "https://download.visualstudio.microsoft.com/download/pr/bc92e2cb/VisualStudio.vsman"
611 }
612 ]
613 },
614 {
615 "id": "Microsoft.VisualStudio.Product.BuildTools",
616 "type": "ChannelProduct",
617 "localizedResources": [
618 { "language": "en-US", "license": "https://go.microsoft.com/fwlink/?LinkId=2179911" }
619 ]
620 }
621 ]
622 }"#;
623
624 const MANIFEST: &str = r#"{
628 "manifestVersion": "1.1",
629 "packages": [
630 { "id": "Microsoft.VC.14.29.16.11.CRT.Headers.base", "version": "14.29.30157", "type": "Vsix",
631 "payloads": [ { "fileName": "old.vsix", "sha256": "aa", "size": 1, "url": "https://example.invalid/old" } ] },
632 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220", "type": "Vsix",
633 "payloads": [ { "fileName": "headers.vsix", "sha256": "B1", "size": 2128977, "url": "https://example.invalid/headers" } ] },
634 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.Resources", "language": "de-DE", "version": "14.44.35220", "type": "Vsix",
635 "payloads": [ { "fileName": "de.vsix", "sha256": "cc", "size": 3, "url": "https://example.invalid/de" } ] },
636 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
637 "payloads": [ { "fileName": "x64.vsix", "sha256": "b2", "size": 51521199, "url": "https://example.invalid/x64" } ] },
638 { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
639 "payloads": [ { "fileName": "arm64.vsix", "sha256": "b3", "size": 49166761, "url": "https://example.invalid/arm64" } ] },
640 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base", "version": "14.44.35226", "type": "Vsix",
641 "payloads": [ { "fileName": "spectre.vsix", "sha256": "dd", "size": 4, "url": "https://example.invalid/spectre" } ] },
642 { "id": "Microsoft.VisualStudio.Component.Windows11SDK", "version": "17.14.35", "type": "Component",
643 "payloads": [ { "fileName": "nothing.vsix", "sha256": "ee", "size": 5, "url": "https://example.invalid/nothing" } ] },
644 { "id": "Win10SDK_10.0.19041", "version": "10.0.19041.4", "type": "Exe",
645 "payloads": [ { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "ff", "size": 6, "url": "https://example.invalid/old-sdk" } ] },
646 { "id": "Win11SDK_10.0.26100", "version": "10.0.26100.15", "type": "Exe",
647 "payloads": [
648 { "fileName": "Installers\\Universal CRT Headers Libraries and Sources-x86_en-us.msi", "sha256": "c1", "size": 589824, "url": "https://example.invalid/ucrt" },
649 { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "c2", "size": 790528, "url": "https://example.invalid/hx86" },
650 { "fileName": "Installers\\Windows SDK Desktop Headers x64-x86_en-us.msi", "sha256": "c3", "size": 450560, "url": "https://example.invalid/hx64" },
651 { "fileName": "Installers\\Windows SDK Desktop Headers arm64-x86_en-us.msi", "sha256": "c4", "size": 446464, "url": "https://example.invalid/harm64" },
652 { "fileName": "Installers\\Windows SDK Desktop Libs x86-x86_en-us.msi", "sha256": "c5", "size": 528384, "url": "https://example.invalid/lx86" },
653 { "fileName": "Installers\\Windows SDK Desktop Libs x64-x86_en-us.msi", "sha256": "c6", "size": 528384, "url": "https://example.invalid/lx64" },
654 { "fileName": "Installers\\Windows SDK Desktop Libs arm64-x86_en-us.msi", "sha256": "c7", "size": 528384, "url": "https://example.invalid/larm64" },
655 { "fileName": "Installers\\Windows SDK OnecoreUap Headers x86-x86_en-us.msi", "sha256": "c8", "size": 495616, "url": "https://example.invalid/onecore" },
656 { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers-x86_en-us.msi", "sha256": "c9", "size": 1060864, "url": "https://example.invalid/store-h" },
657 { "fileName": "Installers\\Windows SDK for Windows Store Apps Libs-x86_en-us.msi", "sha256": "ca", "size": 528384, "url": "https://example.invalid/store-l" },
658 { "fileName": "Installers\\Windows SDK Desktop Tools x64-x86_en-us.msi", "sha256": "cb", "size": 475136, "url": "https://example.invalid/tools" },
659 { "fileName": "0f1a2b3c.cab", "sha256": "cc", "size": 9999, "url": "https://example.invalid/cab" }
660 ] }
661 ]
662 }"#;
663
664 fn target(tuple: &str) -> TargetTuple {
665 tuple.parse().expect("a target this understands")
666 }
667
668 #[test]
669 fn a_channel_says_the_release_the_manifest_and_where_the_licence_is() {
670 let channel = Channel::parse(CHANNEL).expect("a channel");
671 assert_eq!(channel.release, "17.14.41 (September 2026)");
672 assert_eq!(channel.build, "17.14.37710.0");
673 assert_eq!(channel.manifest.name, "VisualStudio.vsman");
674 assert_eq!(channel.manifest.size, 30_443_537);
675 assert!(channel.manifest.sha256.starts_with("6e470016"), "{}", channel.manifest.sha256);
677 assert_eq!(channel.licence, "https://go.microsoft.com/fwlink/?LinkId=2179911");
678 }
679
680 #[test]
681 fn a_channel_with_no_manifest_or_no_licence_in_it_says_which() {
682 let without = CHANNEL.replace("\"type\": \"Manifest\"", "\"type\": \"Bootstrapper\"");
683 assert_eq!(Channel::parse(&without).expect_err("no manifest"), MsvcError::NoManifest);
684
685 let without = CHANNEL.replace("Microsoft.VisualStudio.Product.BuildTools", "Other.Product");
686 assert_eq!(Channel::parse(&without).expect_err("no licence"), MsvcError::NoLicence);
687 }
688
689 #[test]
690 fn the_newest_visual_cpp_and_the_newest_kit_are_the_ones_chosen() {
691 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
692 assert_eq!(chosen.crt, "14.44.35220");
693 assert_eq!(chosen.sdk, "10.0.26100.15");
694 let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
695 assert!(!packages.contains(&"Microsoft.VC.14.29.16.11.CRT.Headers.base"), "{packages:?}");
696 assert!(!packages.contains(&"Win10SDK_10.0.19041"), "{packages:?}");
697 }
698
699 #[test]
700 fn a_selection_is_the_headers_one_library_package_per_chip_and_the_installers() {
701 let one = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
702 assert_eq!(one.files.len(), 1 + 1 + 7);
703
704 let two = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
705 assert_eq!(two.files.len(), one.files.len() + 3);
707 assert!(two.size() > one.size());
708
709 let again =
711 Selection::parse(MANIFEST, &[Chip::Arm64, Chip::X64, Chip::X64]).expect("a selection");
712 assert_eq!(again, two);
713 }
714
715 #[test]
716 fn nothing_that_is_not_a_header_or_a_library_is_chosen() {
717 let chosen = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
718 let names: Vec<&str> = chosen.files.iter().map(|file| leaf(&file.payload.name)).collect();
719 for unwanted in ["spectre.vsix", "de.vsix", "nothing.vsix"] {
720 assert!(!names.contains(&unwanted), "{unwanted} is in {names:?}");
721 }
722 assert!(!names.iter().any(|name| name.contains("Tools")), "{names:?}");
725 assert!(!names.iter().any(|name| name.ends_with(".cab")), "{names:?}");
726 }
727
728 #[test]
729 fn a_missing_package_or_installer_says_which_one_by_name() {
730 let without = MANIFEST.replace("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "Other");
731 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
732 assert_eq!(
733 why,
734 MsvcError::NoPackage("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base".into())
735 );
736
737 let without =
738 MANIFEST.replace("Windows SDK Desktop Libs x64", "Windows SDK Desktop Libs mips");
739 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
740 assert_eq!(
741 why,
742 MsvcError::NoInstaller("Windows SDK Desktop Libs x64-x86_en-us.msi".into())
743 );
744
745 let empty = r#"{ "packages": [] }"#;
746 assert_eq!(
747 Selection::parse(empty, &[Chip::X64]).expect_err("a refusal"),
748 MsvcError::NothingFound("a Visual C++ CRT")
749 );
750 }
751
752 #[test]
753 fn the_windows_path_in_an_installer_name_survives_being_read() {
754 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
755 let ucrt = chosen
756 .files
757 .iter()
758 .find(|file| leaf(&file.payload.name).starts_with("Universal CRT"))
759 .expect("the universal CRT");
760 assert_eq!(
761 ucrt.payload.name,
762 r"Installers\Universal CRT Headers Libraries and Sources-x86_en-us.msi"
763 );
764 assert_eq!(ucrt.version, "10.0.26100.15");
765 }
766
767 #[test]
768 fn a_target_maps_to_the_chip_microsoft_spells_two_ways() {
769 assert_eq!(Chip::of(target("x86_64-windows-msvc")), Some(Chip::X64));
770 assert_eq!(Chip::of(target("aarch64-windows-msvc")), Some(Chip::Arm64));
771 assert_eq!(Chip::of(target("i686-windows-msvc")), Some(Chip::X86));
772 assert_eq!(Chip::of(target("arm64ec-windows-msvc")), None);
774 assert_eq!(Chip::of(target("riscv64-linux-gnu")), None);
775
776 assert_eq!(Chip::Arm64.in_package(), "ARM64");
777 assert_eq!(Chip::Arm64.in_installer(), "arm64");
778 }
779
780 #[test]
781 fn a_dotted_version_is_compared_as_numbers_and_not_as_text() {
782 assert_eq!(
785 newest(["10.0.9.0", "10.0.22000.0"].into_iter()).as_deref(),
786 Some("10.0.22000.0")
787 );
788 assert_eq!(vc_release("Microsoft.VC.14.44.17.14.CRT.Headers.base"), Some("14.44.17.14"));
789 assert_eq!(vc_release("Microsoft.VC.Runtimes.x64.base"), None);
790 }
791
792 #[test]
793 fn a_package_that_lists_its_files_before_it_says_what_it_is_is_refused() {
794 let backwards = r#"{ "packages": [
795 { "payloads": [ { "fileName": "a", "sha256": "b", "size": 1, "url": "c" } ],
796 "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220" } ] }"#;
797 let why = Selection::parse(backwards, &[Chip::X64]).expect_err("a refusal");
798 assert_eq!(
799 why,
800 MsvcError::OutOfOrder("Microsoft.VC.14.44.17.14.CRT.Headers.base".to_owned())
801 );
802 }
803}