1use std::collections::BTreeMap;
90use std::fmt;
91
92use rucc_tuple::{Arch, TargetTuple};
93
94use crate::json::{JsonError, Reader};
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct Payload {
99 pub name: String,
101 pub url: String,
103 pub sha256: String,
105 pub size: u64,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
115pub enum Chip {
116 X86,
118 X64,
120 Arm,
122 Arm64,
124}
125
126impl Chip {
127 #[must_use]
133 pub const fn of(target: TargetTuple) -> Option<Self> {
134 match target.arch() {
135 Arch::X86 => Some(Chip::X86),
136 Arch::X86_64 => Some(Chip::X64),
137 Arch::Arm => Some(Chip::Arm),
138 Arch::Aarch64 => Some(Chip::Arm64),
139 _ => None,
140 }
141 }
142
143 #[must_use]
145 pub const fn in_package(self) -> &'static str {
146 match self {
147 Chip::X86 => "x86",
148 Chip::X64 => "x64",
149 Chip::Arm => "arm",
150 Chip::Arm64 => "ARM64",
152 }
153 }
154
155 #[must_use]
157 pub const fn in_installer(self) -> &'static str {
158 match self {
159 Chip::X86 => "x86",
160 Chip::X64 => "x64",
161 Chip::Arm => "arm",
162 Chip::Arm64 => "arm64",
163 }
164 }
165
166 #[must_use]
173 pub const fn in_tree(self) -> &'static str {
174 match self {
175 Chip::X86 => "x86",
176 Chip::X64 => "x86_64",
177 Chip::Arm => "aarch",
178 Chip::Arm64 => "aarch64",
179 }
180 }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct Channel {
186 pub release: String,
188 pub build: String,
190 pub manifest: Payload,
192 pub licence: String,
196}
197
198impl Channel {
199 pub fn parse(text: &str) -> Result<Self, MsvcError> {
206 let mut release = String::new();
207 let mut build = String::new();
208 let mut manifest = None;
209 let mut licence = String::new();
210
211 let mut reader = Reader::new(text);
212 reader.enter_object()?;
213 while let Some(key) = reader.next_key()? {
214 match &*key {
215 "info" => {
216 reader.enter_object()?;
217 while let Some(field) = reader.next_key()? {
218 match &*field {
219 "productDisplayVersion" => release = reader.string()?.into_owned(),
220 "buildVersion" => build = reader.string()?.into_owned(),
221 _ => reader.skip()?,
222 }
223 }
224 }
225 "channelItems" => {
226 reader.enter_array()?;
227 while reader.next_item()? {
228 let item = channel_item(&mut reader)?;
229 if item.kind == "Manifest" {
230 manifest = item.payload;
231 } else if item.id == BUILD_TOOLS && !item.licence.is_empty() {
232 licence = item.licence;
233 }
234 }
235 }
236 _ => reader.skip()?,
237 }
238 }
239
240 let manifest = manifest.ok_or(MsvcError::NoManifest)?;
241 if licence.is_empty() {
242 return Err(MsvcError::NoLicence);
243 }
244 Ok(Channel { release, build, manifest, licence })
245 }
246}
247
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
256pub struct PinnedBuild {
257 pub build: &'static str,
259 pub channel: &'static str,
261 pub channel_sha256: &'static str,
263 pub manifest: &'static str,
265 pub manifest_sha256: &'static str,
267}
268
269pub const PINNED_BUILD: PinnedBuild = PinnedBuild {
275 build: "17.14.37710.0",
276 channel: "https://download.visualstudio.microsoft.com/download/pr/\
277 bc92e2cb-33de-4a0c-995d-efa817f16b16/\
278 0dbdfd40c17757e64fc9f72cd9954ec8471c04fd8461a77806e5291e23239ac0/\
279 VisualStudio.17.Release.chman",
280 channel_sha256: "fca418ba94ffbcfb7a2b25f10f16f39dd09660568d21eef4bd3f274cb0b27b8c",
281 manifest: "https://download.visualstudio.microsoft.com/download/pr/\
282 bc92e2cb-33de-4a0c-995d-efa817f16b16/\
283 6e470016e4324c84c255ffd0beb3767d17ec89cc8561e9409ee3e1f6d29400f5/\
284 VisualStudio.vsman",
285 manifest_sha256: "f0a50ea157222c29abd5ea6ff01bfc3c33b04e011c5e45ee2ca38ef0778e5643",
286};
287
288const BUILD_TOOLS: &str = "Microsoft.VisualStudio.Product.BuildTools";
291
292struct ChannelItem {
294 id: String,
295 kind: String,
296 payload: Option<Payload>,
297 licence: String,
298}
299
300fn channel_item(reader: &mut Reader<'_>) -> Result<ChannelItem, MsvcError> {
302 let mut item = ChannelItem {
303 id: String::new(),
304 kind: String::new(),
305 payload: None,
306 licence: String::new(),
307 };
308 reader.enter_object()?;
309 while let Some(field) = reader.next_key()? {
310 match &*field {
311 "id" => item.id = reader.string()?.into_owned(),
312 "type" => item.kind = reader.string()?.into_owned(),
313 "payloads" => {
314 let mut all = payloads(reader)?;
315 item.payload = (!all.is_empty()).then(|| all.remove(0));
316 }
317 "localizedResources" => {
320 reader.enter_array()?;
321 while reader.next_item()? {
322 reader.enter_object()?;
323 while let Some(inner) = reader.next_key()? {
324 if inner == "license" && item.licence.is_empty() {
325 item.licence = reader.string()?.into_owned();
326 } else {
327 reader.skip()?;
328 }
329 }
330 }
331 }
332 _ => reader.skip()?,
333 }
334 }
335 Ok(item)
336}
337
338fn payloads(reader: &mut Reader<'_>) -> Result<Vec<Payload>, MsvcError> {
340 let mut all = Vec::new();
341 reader.enter_array()?;
342 while reader.next_item()? {
343 let mut one =
344 Payload { name: String::new(), url: String::new(), sha256: String::new(), size: 0 };
345 reader.enter_object()?;
346 while let Some(field) = reader.next_key()? {
347 match &*field {
348 "fileName" => one.name = reader.string()?.into_owned(),
349 "url" => one.url = reader.string()?.into_owned(),
350 "sha256" => one.sha256 = reader.string()?.to_ascii_lowercase(),
353 "size" => one.size = reader.integer()?,
354 _ => reader.skip()?,
355 }
356 }
357 all.push(one);
358 }
359 Ok(all)
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct Wanted {
365 pub package: String,
367 pub version: String,
369 pub payload: Payload,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq)]
375pub struct Selection {
376 pub crt: String,
378 pub sdk: String,
380 pub files: Vec<Wanted>,
382 pub cabs: Vec<Wanted>,
389}
390
391impl Selection {
392 pub fn parse(text: &str, chips: &[Chip]) -> Result<Self, MsvcError> {
402 let found = collect(text)?;
403 let crt = newest(found.keys().filter_map(|id| vc_release(id)))
404 .ok_or(MsvcError::NothingFound("a Visual C++ CRT"))?;
405 let sdk_id = newest_sdk(&found).ok_or(MsvcError::NothingFound("a Windows SDK"))?;
406
407 let mut files = Vec::new();
408 let headers = format!("Microsoft.VC.{crt}.CRT.Headers.base");
409 let mut wanted = vec![headers.clone()];
410 for chip in sorted(chips) {
411 wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Desktop.base", chip.in_package()));
414 wanted.push(format!("Microsoft.VC.{crt}.CRT.{}.Store.base", chip.in_package()));
415 }
416 let mut crt_version = String::new();
419 for id in wanted {
420 let package = found.get(&id).ok_or_else(|| MsvcError::NoPackage(id.clone()))?;
421 if id == headers {
422 crt_version.clone_from(&package.version);
423 }
424 for payload in &package.payloads {
425 files.push(Wanted {
426 package: id.clone(),
427 version: package.version.clone(),
428 payload: payload.clone(),
429 });
430 }
431 }
432
433 let sdk = found.get(&sdk_id).ok_or_else(|| MsvcError::NoPackage(sdk_id.clone()))?;
434 for installer in installers(chips) {
435 let payload = sdk
436 .payloads
437 .iter()
438 .find(|payload| leaf(&payload.name) == installer)
439 .ok_or_else(|| MsvcError::NoInstaller(installer.clone()))?;
440 files.push(Wanted {
441 package: sdk_id.clone(),
442 version: sdk.version.clone(),
443 payload: payload.clone(),
444 });
445 }
446
447 let mut cabs: Vec<Wanted> = sdk
448 .payloads
449 .iter()
450 .filter(|payload| leaf(&payload.name).to_ascii_lowercase().ends_with(".cab"))
451 .map(|payload| Wanted {
452 package: sdk_id.clone(),
453 version: sdk.version.clone(),
454 payload: payload.clone(),
455 })
456 .collect();
457 cabs.sort_by(|a, b| a.payload.name.cmp(&b.payload.name));
458
459 files.sort_by(|a, b| (&a.package, &a.payload.name).cmp(&(&b.package, &b.payload.name)));
460 Ok(Selection { crt: crt_version, sdk: sdk.version.clone(), files, cabs })
461 }
462
463 #[must_use]
471 pub fn cab(&self, name: &str) -> Option<&Wanted> {
472 self.cabs.iter().find(|cab| leaf(&cab.payload.name).eq_ignore_ascii_case(leaf(name)))
473 }
474
475 #[must_use]
483 pub fn size(&self) -> u64 {
484 self.files.iter().map(|file| file.payload.size).sum()
485 }
486}
487
488fn installers(chips: &[Chip]) -> Vec<String> {
497 let mut all = vec![
498 "Universal CRT Headers Libraries and Sources-x86_en-us.msi".to_owned(),
499 "Windows SDK Desktop Headers x86-x86_en-us.msi".to_owned(),
500 "Windows SDK OnecoreUap Headers x86-x86_en-us.msi".to_owned(),
501 "Windows SDK for Windows Store Apps Headers-x86_en-us.msi".to_owned(),
502 "Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi".to_owned(),
503 "Windows SDK for Windows Store Apps Libs-x86_en-us.msi".to_owned(),
504 ];
505 for chip in sorted(chips) {
506 let arch = chip.in_installer();
507 all.push(format!("Windows SDK Desktop Headers {arch}-x86_en-us.msi"));
508 all.push(format!("Windows SDK Desktop Libs {arch}-x86_en-us.msi"));
509 }
510 all.sort();
511 all.dedup();
512 all
513}
514
515fn sorted(chips: &[Chip]) -> Vec<Chip> {
518 let mut all = chips.to_vec();
519 all.sort_unstable();
520 all.dedup();
521 all
522}
523
524#[derive(Debug)]
526struct Package {
527 version: String,
528 payloads: Vec<Payload>,
529}
530
531fn collect(text: &str) -> Result<BTreeMap<String, Package>, MsvcError> {
538 let mut found: BTreeMap<String, Package> = BTreeMap::new();
539 let mut reader = Reader::new(text);
540 reader.enter_object()?;
541 while let Some(key) = reader.next_key()? {
542 if key != "packages" {
543 reader.skip()?;
544 continue;
545 }
546 reader.enter_array()?;
547 while reader.next_item()? {
548 let mut id = String::new();
549 let mut version = String::new();
550 let mut keep = None;
551 let mut listed = false;
552 let mut read = false;
553 reader.enter_object()?;
554 while let Some(field) = reader.next_key()? {
555 match &*field {
556 "id" => {
557 id = reader.string()?.into_owned();
558 keep = Some(interesting(&id));
559 }
560 "version" => version = reader.string()?.into_owned(),
561 "payloads" => {
562 listed = true;
563 read = keep == Some(true);
564 if read {
565 let all = payloads(&mut reader)?;
566 found
567 .entry(id.clone())
568 .or_insert(Package { version: version.clone(), payloads: all });
569 } else {
570 reader.skip()?;
571 }
572 }
573 _ => reader.skip()?,
574 }
575 }
576 if keep == Some(true) && listed && !read && !found.contains_key(&id) {
580 return Err(MsvcError::OutOfOrder(id));
581 }
582 if let Some(package) = found.get_mut(&id) {
585 if package.version.is_empty() {
586 package.version.clone_from(&version);
587 }
588 }
589 }
590 }
591 Ok(found)
592}
593
594fn interesting(id: &str) -> bool {
599 (id.starts_with("Microsoft.VC.") && id.contains(".CRT.") && id.ends_with(".base"))
600 || id.starts_with("Win10SDK_10.0.")
601 || id.starts_with("Win11SDK_10.0.")
602}
603
604fn vc_release(id: &str) -> Option<&str> {
610 let rest = id.strip_prefix("Microsoft.VC.")?;
611 let at = rest.find(".CRT.")?;
612 let release = &rest[..at];
613 release
614 .split('.')
615 .all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
616 .then_some(release)
617}
618
619fn newest<'a>(all: impl Iterator<Item = &'a str>) -> Option<String> {
624 all.max_by(|left, right| numbers(left).cmp(&numbers(right))).map(ToOwned::to_owned)
625}
626
627fn numbers(text: &str) -> Vec<u64> {
629 text.split('.').map(|part| part.parse().unwrap_or(0)).collect()
630}
631
632fn newest_sdk(found: &BTreeMap<String, Package>) -> Option<String> {
638 found
639 .iter()
640 .filter(|(id, _)| id.starts_with("Win10SDK_10.0.") || id.starts_with("Win11SDK_10.0."))
641 .max_by_key(|(id, package)| {
642 let build = id.rsplit('.').next().and_then(|last| last.parse::<u64>().ok());
643 (build.unwrap_or(0), numbers(&package.version))
644 })
645 .map(|(id, _)| id.clone())
646}
647
648fn leaf(name: &str) -> &str {
650 name.rsplit('\\').next().unwrap_or(name)
651}
652
653#[derive(Debug, Clone, PartialEq, Eq)]
655pub enum MsvcError {
656 Json {
661 at: usize,
663 wanted: &'static str,
665 },
666 NoManifest,
668 NoLicence,
671 NothingFound(&'static str),
673 NoPackage(String),
675 NoInstaller(String),
677 OutOfOrder(String),
680}
681
682impl From<JsonError> for MsvcError {
683 fn from(why: JsonError) -> Self {
684 MsvcError::Json { at: why.at, wanted: why.wanted }
685 }
686}
687
688impl fmt::Display for MsvcError {
689 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
690 match self {
691 MsvcError::Json { at, wanted } => {
692 write!(f, "this is not the document it was taken for: {wanted} at byte {at} of it")
693 }
694 MsvcError::NoManifest => {
695 write!(f, "this channel names no installer manifest, so it is not a channel")
696 }
697 MsvcError::NoLicence => write!(
698 f,
699 "this channel names no licence for the build tools, and the download it describes \
700 is one nobody may make without reading one"
701 ),
702 MsvcError::NothingFound(what) => {
703 write!(f, "this manifest has no {what} in it")
704 }
705 MsvcError::NoPackage(id) => {
706 write!(
707 f,
708 "this manifest has no {id}, which is a package an MSVC sysroot is made of"
709 )
710 }
711 MsvcError::NoInstaller(name) => write!(
712 f,
713 "the Windows SDK in this manifest has no {name} in it, which is an installer an \
714 MSVC sysroot is made of"
715 ),
716 MsvcError::OutOfOrder(id) => write!(
717 f,
718 "{id} lists its files before it says what it is, which this reader relies on the \
719 manifest not doing"
720 ),
721 }
722 }
723}
724
725impl std::error::Error for MsvcError {}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 #[test]
734 fn the_pinned_build_names_two_storage_urls_and_holds_each_to_a_hash() {
735 let pin = PINNED_BUILD;
736 for (url, sha256, file) in [
737 (pin.channel, pin.channel_sha256, "VisualStudio.17.Release.chman"),
738 (pin.manifest, pin.manifest_sha256, "VisualStudio.vsman"),
739 ] {
740 assert!(!url.contains(' '), "{url}");
742 assert!(url.starts_with("https://download.visualstudio.microsoft.com/"), "{url}");
743 assert!(url.ends_with(&format!("/{file}")), "{url}");
744 assert_eq!(sha256.len(), 64, "{sha256}");
745 assert!(sha256.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)));
746 }
747 assert!(!pin.manifest.contains(pin.manifest_sha256));
750 assert!(pin.build.starts_with("17."));
751 }
752
753 const CHANNEL: &str = r#"{
756 "manifestVersion": "1.1",
757 "info": { "buildVersion": "17.14.37710.0", "productDisplayVersion": "17.14.41 (September 2026)" },
758 "channelItems": [
759 {
760 "id": "Microsoft.VisualStudio.Manifests.VisualStudio",
761 "version": "17.14.37710.0",
762 "type": "Manifest",
763 "payloads": [
764 {
765 "fileName": "VisualStudio.vsman",
766 "sha256": "6E470016E4324C84C255FFD0BEB3767D17EC89CC8561E9409EE3E1F6D29400F5",
767 "size": 30443537,
768 "url": "https://download.visualstudio.microsoft.com/download/pr/bc92e2cb/VisualStudio.vsman"
769 }
770 ]
771 },
772 {
773 "id": "Microsoft.VisualStudio.Product.BuildTools",
774 "type": "ChannelProduct",
775 "localizedResources": [
776 { "language": "en-US", "license": "https://go.microsoft.com/fwlink/?LinkId=2179911" }
777 ]
778 }
779 ]
780 }"#;
781
782 const MANIFEST: &str = r#"{
786 "manifestVersion": "1.1",
787 "packages": [
788 { "id": "Microsoft.VC.14.29.16.11.CRT.Headers.base", "version": "14.29.30157", "type": "Vsix",
789 "payloads": [ { "fileName": "old.vsix", "sha256": "aa", "size": 1, "url": "https://example.invalid/old" } ] },
790 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220", "type": "Vsix",
791 "payloads": [ { "fileName": "headers.vsix", "sha256": "B1", "size": 2128977, "url": "https://example.invalid/headers" } ] },
792 { "id": "Microsoft.VC.14.44.17.14.CRT.Headers.Resources", "language": "de-DE", "version": "14.44.35220", "type": "Vsix",
793 "payloads": [ { "fileName": "de.vsix", "sha256": "cc", "size": 3, "url": "https://example.invalid/de" } ] },
794 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
795 "payloads": [ { "fileName": "x64.vsix", "sha256": "b2", "size": 51521199, "url": "https://example.invalid/x64" } ] },
796 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Store.base", "version": "14.44.35226", "type": "Vsix",
797 "payloads": [ { "fileName": "x64-store.vsix", "sha256": "b4", "size": 28032384, "url": "https://example.invalid/x64-store" } ] },
798 { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Desktop.base", "version": "14.44.35226", "type": "Vsix",
799 "payloads": [ { "fileName": "arm64.vsix", "sha256": "b3", "size": 49166761, "url": "https://example.invalid/arm64" } ] },
800 { "id": "Microsoft.VC.14.44.17.14.CRT.ARM64.Store.base", "version": "14.44.35226", "type": "Vsix",
801 "payloads": [ { "fileName": "arm64-store.vsix", "sha256": "b5", "size": 26214400, "url": "https://example.invalid/arm64-store" } ] },
802 { "id": "Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base", "version": "14.44.35226", "type": "Vsix",
803 "payloads": [ { "fileName": "spectre.vsix", "sha256": "dd", "size": 4, "url": "https://example.invalid/spectre" } ] },
804 { "id": "Microsoft.VisualStudio.Component.Windows11SDK", "version": "17.14.35", "type": "Component",
805 "payloads": [ { "fileName": "nothing.vsix", "sha256": "ee", "size": 5, "url": "https://example.invalid/nothing" } ] },
806 { "id": "Win10SDK_10.0.19041", "version": "10.0.19041.4", "type": "Exe",
807 "payloads": [ { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "ff", "size": 6, "url": "https://example.invalid/old-sdk" } ] },
808 { "id": "Win11SDK_10.0.26100", "version": "10.0.26100.15", "type": "Exe",
809 "payloads": [
810 { "fileName": "Installers\\Universal CRT Headers Libraries and Sources-x86_en-us.msi", "sha256": "c1", "size": 589824, "url": "https://example.invalid/ucrt" },
811 { "fileName": "Installers\\Windows SDK Desktop Headers x86-x86_en-us.msi", "sha256": "c2", "size": 790528, "url": "https://example.invalid/hx86" },
812 { "fileName": "Installers\\Windows SDK Desktop Headers x64-x86_en-us.msi", "sha256": "c3", "size": 450560, "url": "https://example.invalid/hx64" },
813 { "fileName": "Installers\\Windows SDK Desktop Headers arm64-x86_en-us.msi", "sha256": "c4", "size": 446464, "url": "https://example.invalid/harm64" },
814 { "fileName": "Installers\\Windows SDK Desktop Libs x86-x86_en-us.msi", "sha256": "c5", "size": 528384, "url": "https://example.invalid/lx86" },
815 { "fileName": "Installers\\Windows SDK Desktop Libs x64-x86_en-us.msi", "sha256": "c6", "size": 528384, "url": "https://example.invalid/lx64" },
816 { "fileName": "Installers\\Windows SDK Desktop Libs arm64-x86_en-us.msi", "sha256": "c7", "size": 528384, "url": "https://example.invalid/larm64" },
817 { "fileName": "Installers\\Windows SDK OnecoreUap Headers x86-x86_en-us.msi", "sha256": "c8", "size": 495616, "url": "https://example.invalid/onecore" },
818 { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers-x86_en-us.msi", "sha256": "c9", "size": 1060864, "url": "https://example.invalid/store-h" },
819 { "fileName": "Installers\\Windows SDK for Windows Store Apps Headers OnecoreUap-x86_en-us.msi", "sha256": "ce", "size": 471040, "url": "https://example.invalid/store-onecore" },
820 { "fileName": "Installers\\Windows SDK for Windows Store Apps Libs-x86_en-us.msi", "sha256": "ca", "size": 528384, "url": "https://example.invalid/store-l" },
821 { "fileName": "Installers\\Windows SDK Desktop Tools x64-x86_en-us.msi", "sha256": "cb", "size": 475136, "url": "https://example.invalid/tools" },
822 { "fileName": "Installers\\0f1a2b3c.cab", "sha256": "cc", "size": 9999, "url": "https://example.invalid/cab" },
823 { "fileName": "Installers\\7e6d5c4b.cab", "sha256": "cd", "size": 8888, "url": "https://example.invalid/other-cab" }
824 ] }
825 ]
826 }"#;
827
828 fn target(tuple: &str) -> TargetTuple {
829 tuple.parse().expect("a target this understands")
830 }
831
832 #[test]
833 fn a_channel_says_the_release_the_manifest_and_where_the_licence_is() {
834 let channel = Channel::parse(CHANNEL).expect("a channel");
835 assert_eq!(channel.release, "17.14.41 (September 2026)");
836 assert_eq!(channel.build, "17.14.37710.0");
837 assert_eq!(channel.manifest.name, "VisualStudio.vsman");
838 assert_eq!(channel.manifest.size, 30_443_537);
839 assert!(channel.manifest.sha256.starts_with("6e470016"), "{}", channel.manifest.sha256);
841 assert_eq!(channel.licence, "https://go.microsoft.com/fwlink/?LinkId=2179911");
842 }
843
844 #[test]
845 fn a_channel_with_no_manifest_or_no_licence_in_it_says_which() {
846 let without = CHANNEL.replace("\"type\": \"Manifest\"", "\"type\": \"Bootstrapper\"");
847 assert_eq!(Channel::parse(&without).expect_err("no manifest"), MsvcError::NoManifest);
848
849 let without = CHANNEL.replace("Microsoft.VisualStudio.Product.BuildTools", "Other.Product");
850 assert_eq!(Channel::parse(&without).expect_err("no licence"), MsvcError::NoLicence);
851 }
852
853 #[test]
854 fn the_newest_visual_cpp_and_the_newest_kit_are_the_ones_chosen() {
855 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
856 assert_eq!(chosen.crt, "14.44.35220");
857 assert_eq!(chosen.sdk, "10.0.26100.15");
858 let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
859 assert!(!packages.contains(&"Microsoft.VC.14.29.16.11.CRT.Headers.base"), "{packages:?}");
860 assert!(!packages.contains(&"Win10SDK_10.0.19041"), "{packages:?}");
861 }
862
863 #[test]
864 fn a_selection_is_the_headers_two_library_packages_per_chip_and_the_installers() {
865 let one = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
866 assert_eq!(one.files.len(), 1 + 2 + 8);
867
868 let two = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
869 assert_eq!(two.files.len(), one.files.len() + 4);
871 assert!(two.size() > one.size());
872
873 let again =
875 Selection::parse(MANIFEST, &[Chip::Arm64, Chip::X64, Chip::X64]).expect("a selection");
876 assert_eq!(again, two);
877 }
878
879 #[test]
880 fn nothing_that_is_not_a_header_or_a_library_is_chosen() {
881 let chosen = Selection::parse(MANIFEST, &[Chip::X64, Chip::Arm64]).expect("a selection");
882 let names: Vec<&str> = chosen.files.iter().map(|file| leaf(&file.payload.name)).collect();
883 for unwanted in ["spectre.vsix", "de.vsix", "nothing.vsix"] {
884 assert!(!names.contains(&unwanted), "{unwanted} is in {names:?}");
885 }
886 assert!(!names.iter().any(|name| name.contains("Tools")), "{names:?}");
889 assert!(!names.iter().any(|name| name.ends_with(".cab")), "{names:?}");
890 }
891
892 #[test]
893 fn the_store_package_is_taken_for_its_import_libraries_and_the_spectre_one_is_not() {
894 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
895 let packages: Vec<&str> = chosen.files.iter().map(|file| file.package.as_str()).collect();
896 assert!(packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Store.base"), "{packages:?}");
899 assert!(
900 packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base"),
901 "{packages:?}"
902 );
903 assert!(
904 !packages.contains(&"Microsoft.VC.14.44.17.14.CRT.x64.Desktop.spectre.base"),
905 "{packages:?}"
906 );
907 }
908
909 #[test]
910 fn a_cabinet_is_found_by_the_name_an_installer_gives_it() {
911 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
912 assert_eq!(chosen.cabs.len(), 2);
913 let cab = chosen.cab("0f1a2b3c.cab").expect("the cabinet");
916 assert_eq!(cab.payload.name, r"Installers\0f1a2b3c.cab");
917 assert_eq!(cab.payload.url, "https://example.invalid/cab");
918 assert_eq!(cab.package, "Win11SDK_10.0.26100");
919 assert_eq!(chosen.cab("0F1A2B3C.CAB"), Some(cab));
921 assert_eq!(chosen.cab("nothing.cab"), None);
922 }
923
924 #[test]
925 fn a_missing_package_or_installer_says_which_one_by_name() {
926 let without = MANIFEST.replace("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base", "Other");
927 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
928 assert_eq!(
929 why,
930 MsvcError::NoPackage("Microsoft.VC.14.44.17.14.CRT.x64.Desktop.base".into())
931 );
932
933 let without =
934 MANIFEST.replace("Windows SDK Desktop Libs x64", "Windows SDK Desktop Libs mips");
935 let why = Selection::parse(&without, &[Chip::X64]).expect_err("a refusal");
936 assert_eq!(
937 why,
938 MsvcError::NoInstaller("Windows SDK Desktop Libs x64-x86_en-us.msi".into())
939 );
940
941 let empty = r#"{ "packages": [] }"#;
942 assert_eq!(
943 Selection::parse(empty, &[Chip::X64]).expect_err("a refusal"),
944 MsvcError::NothingFound("a Visual C++ CRT")
945 );
946 }
947
948 #[test]
949 fn the_windows_path_in_an_installer_name_survives_being_read() {
950 let chosen = Selection::parse(MANIFEST, &[Chip::X64]).expect("a selection");
951 let ucrt = chosen
952 .files
953 .iter()
954 .find(|file| leaf(&file.payload.name).starts_with("Universal CRT"))
955 .expect("the universal CRT");
956 assert_eq!(
957 ucrt.payload.name,
958 r"Installers\Universal CRT Headers Libraries and Sources-x86_en-us.msi"
959 );
960 assert_eq!(ucrt.version, "10.0.26100.15");
961 }
962
963 #[test]
964 fn a_target_maps_to_the_chip_microsoft_spells_two_ways() {
965 assert_eq!(Chip::of(target("x86_64-windows-msvc")), Some(Chip::X64));
966 assert_eq!(Chip::of(target("aarch64-windows-msvc")), Some(Chip::Arm64));
967 assert_eq!(Chip::of(target("i686-windows-msvc")), Some(Chip::X86));
968 assert_eq!(Chip::of(target("arm64ec-windows-msvc")), None);
970 assert_eq!(Chip::of(target("riscv64-linux-gnu")), None);
971
972 assert_eq!(Chip::Arm64.in_package(), "ARM64");
973 assert_eq!(Chip::Arm64.in_installer(), "arm64");
974 }
975
976 #[test]
977 fn a_dotted_version_is_compared_as_numbers_and_not_as_text() {
978 assert_eq!(
981 newest(["10.0.9.0", "10.0.22000.0"].into_iter()).as_deref(),
982 Some("10.0.22000.0")
983 );
984 assert_eq!(vc_release("Microsoft.VC.14.44.17.14.CRT.Headers.base"), Some("14.44.17.14"));
985 assert_eq!(vc_release("Microsoft.VC.Runtimes.x64.base"), None);
986 }
987
988 #[test]
989 fn a_package_that_lists_its_files_before_it_says_what_it_is_is_refused() {
990 let backwards = r#"{ "packages": [
991 { "payloads": [ { "fileName": "a", "sha256": "b", "size": 1, "url": "c" } ],
992 "id": "Microsoft.VC.14.44.17.14.CRT.Headers.base", "version": "14.44.35220" } ] }"#;
993 let why = Selection::parse(backwards, &[Chip::X64]).expect_err("a refusal");
994 assert_eq!(
995 why,
996 MsvcError::OutOfOrder("Microsoft.VC.14.44.17.14.CRT.Headers.base".to_owned())
997 );
998 }
999}