#![cfg(target_os = "windows")]
#![forbid(warnings)]
#![forbid(future_incompatible)]
#![deny(unused)]
#![forbid(box_pointers)]
#![forbid(missing_copy_implementations)]
#![forbid(missing_debug_implementations)]
#![forbid(missing_docs)]
#![forbid(trivial_casts)]
#![forbid(trivial_numeric_casts)]
#![forbid(unused_import_braces)]
#![deny(unused_qualifications)]
#![forbid(unused_results)]
#![forbid(variant_size_differences)]
#![cfg_attr(feature = "cargo-clippy", forbid(clippy))]
#![cfg_attr(feature = "cargo-clippy", deny(clippy_pedantic))]
#![cfg_attr(feature = "cargo-clippy", forbid(clippy_cargo))]
#![cfg_attr(feature = "cargo-clippy", forbid(clippy_complexity))]
#![cfg_attr(feature = "cargo-clippy", deny(clippy_correctness))]
#![cfg_attr(feature = "cargo-clippy", deny(clippy_perf))]
#![cfg_attr(feature = "cargo-clippy", forbid(clippy_style))]
extern crate chrono;
extern crate semver;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate url;
extern crate url_serde;
extern crate winapi;
use chrono::offset::Utc;
use chrono::DateTime;
use semver::Version;
use serde::de::{Unexpected, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::ffi::OsString;
use std::fmt::{self, Display, Formatter};
use std::io::{self, ErrorKind};
use std::iter;
use std::ops::Range;
use std::os::windows::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::ptr;
use std::slice;
use std::str;
use url::Url;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct FourPointVersion {
major: u16,
minor: u16,
revision: u16,
build: u16,
}
#[derive(Clone, Debug)]
pub struct Config {
prerelease: bool,
products: Vec<String>,
requires: Vec<String>,
requires_any: bool,
version: Option<Range<FourPointVersion>>,
latest: bool,
}
#[cfg_attr(feature = "cargo-clippy", allow(similar_names))]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallInfo {
instance_id: String,
install_date: DateTime<Utc>,
installation_name: String,
installation_path: PathBuf,
installation_version: FourPointVersion,
product_id: String,
product_path: PathBuf,
is_prerelease: bool,
display_name: String,
description: String,
channel_id: String,
channel_path: PathBuf,
#[serde(with = "url_serde")]
channel_uri: Url,
engine_path: PathBuf,
#[serde(with = "url_serde")]
release_notes: Url,
#[serde(with = "url_serde")]
third_party_notices: Url,
update_date: DateTime<Utc>,
catalog: InstallCatalog,
properties: InstallProperties,
}
#[cfg_attr(feature = "cargo-clippy", allow(similar_names))]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallCatalog {
build_branch: String,
build_version: FourPointVersion,
id: String,
local_build: String,
manifest_name: String,
manifest_type: String,
product_display_version: String,
product_line: String,
product_line_version: String,
product_milestone: String,
#[serde(deserialize_with = "deserialize_uppercase_bool")]
#[serde(serialize_with = "serialize_uppercase_bool")]
product_milestone_is_pre_release: bool,
product_name: String,
product_patch_version: String,
product_pre_release_milestone_suffix: String,
product_release: String,
product_semantic_version: Version,
required_engine_version: FourPointVersion,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InstallProperties {
campaign_id: String,
channel_manifest_id: String,
nickname: String,
setup_engine_file_path: PathBuf,
}
fn deserialize_uppercase_bool<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<bool, D::Error> {
struct UppercaseBoolVisitor;
impl<'de> Visitor<'de> for UppercaseBoolVisitor {
type Value = bool;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
write!(formatter, r#"a string, either `"True"` or `"False"`"#)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
let lower = v.to_lowercase();
let lower_trim = lower.trim();
if lower_trim == "true" {
Ok(true)
} else if lower_trim == "false" {
Ok(false)
} else {
Err(E::invalid_value(Unexpected::Str(v), &self))
}
}
}
deserializer.deserialize_str(UppercaseBoolVisitor)
}
#[cfg_attr(feature = "cargo-clippy", allow(trivially_copy_pass_by_ref))]
fn serialize_uppercase_bool<S: Serializer>(
boolean: &bool,
serializer: S,
) -> Result<S::Ok, S::Error> {
if *boolean {
serializer.serialize_str("True")
} else {
serializer.serialize_str("False")
}
}
impl FourPointVersion {
pub fn new(major: u16, minor: u16, revision: u16, build: u16) -> Self {
Self {
major,
minor,
revision,
build,
}
}
pub fn major(self) -> u16 {
self.major
}
pub fn minor(self) -> u16 {
self.minor
}
pub fn revision(self) -> u16 {
self.revision
}
pub fn build(self) -> u16 {
self.build
}
}
impl<'de> Deserialize<'de> for FourPointVersion {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct FourPointVersionVisitor;
impl<'de> Visitor<'de> for FourPointVersionVisitor {
type Value = FourPointVersion;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
write!(
formatter,
"one to four 16-bit unsigned integers separated by a period (`.`)"
)
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
fn parse_number<E: serde::de::Error>(
visitor: &FourPointVersionVisitor,
chunk: &str,
) -> Result<u16, E> {
u16::from_str_radix(chunk, 10)
.map_err(|_| E::invalid_value(Unexpected::Str(chunk), visitor))
}
let iter = v.split('.');
let len = iter.clone().count();
if len < 1 || len > 4 {
Err(E::invalid_length(len, &self))
} else {
let mut version_getter = iter.chain(iter::repeat("0"));
Ok(FourPointVersion::new(
parse_number(&self, version_getter.next().unwrap())?,
parse_number(&self, version_getter.next().unwrap())?,
parse_number(&self, version_getter.next().unwrap())?,
parse_number(&self, version_getter.next().unwrap())?,
))
}
}
}
deserializer.deserialize_str(FourPointVersionVisitor)
}
}
impl Display for FourPointVersion {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(
f,
"{}.{}.{}.{}",
self.major, self.minor, self.revision, self.build
)
}
}
impl Serialize for FourPointVersion {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self)
}
}
impl Config {
pub fn new() -> Self {
Self {
prerelease: false,
products: Vec::new(),
requires: Vec::new(),
requires_any: false,
version: None,
latest: false,
}
}
pub fn find_prerelease_versions(&mut self, prerelease: bool) -> &mut Self {
self.prerelease = prerelease;
self
}
pub fn whitelist_product_id<T: ToString + ?Sized>(&mut self, product_id: &T) -> &mut Self {
self.products.push(product_id.to_string());
self
}
pub fn whitelist_component_id<T: ToString + ?Sized>(&mut self, component_id: &T) -> &mut Self {
self.requires.push(component_id.to_string());
self
}
pub fn require_any_component(&mut self, require_any: bool) -> &mut Self {
self.requires_any = require_any;
self
}
pub fn version_number_range(&mut self, range: Range<FourPointVersion>) -> &mut Self {
self.version = Some(range);
self
}
pub fn only_latest_versions(&mut self, latest: bool) -> &mut Self {
self.latest = latest;
self
}
pub fn run_default_path(&self) -> io::Result<Vec<InstallInfo>> {
use winapi::ctypes::c_void;
use winapi::shared::ntdef::PWSTR;
use winapi::shared::winerror::S_OK;
use winapi::um::combaseapi::CoTaskMemFree;
use winapi::um::knownfolders::{FOLDERID_ProgramData, FOLDERID_ProgramFilesX86};
use winapi::um::shlobj::SHGetKnownFolderPath;
use winapi::um::shtypes::REFKNOWNFOLDERID;
fn get_known_folder_path(id: REFKNOWNFOLDERID) -> io::Result<PathBuf> {
struct KnownFolderPath(PWSTR);
impl Drop for KnownFolderPath {
fn drop(&mut self) {
unsafe {
CoTaskMemFree(self.0 as *mut c_void);
}
}
}
unsafe {
let mut path = KnownFolderPath(ptr::null_mut());
let hres = SHGetKnownFolderPath(id, 0, ptr::null_mut(), &mut path.0);
if hres == S_OK {
let mut wide_string = path.0;
let mut len = 0;
while wide_string.read() != 0 {
wide_string = wide_string.offset(1);
len += 1;
}
let ws_slice = slice::from_raw_parts(path.0, len);
let os_string = OsString::from_wide(ws_slice);
Ok(Path::new(&os_string).to_owned())
} else {
Err(io::Error::last_os_error())
}
}
}
let pd = get_known_folder_path(&FOLDERID_ProgramData)
.map(|p| p.join(r"chocolatey\bin\vswhere.exe"))?;
self.run_custom_path(pd).or_else(|e| {
if e.kind() == ErrorKind::NotFound {
get_known_folder_path(&FOLDERID_ProgramFilesX86)
.map(|p| p.join(r"Microsoft Visual Studio\Installer\vswhere.exe"))
.and_then(|p| self.run_custom_path(p))
} else {
Err(e)
}
})
}
pub fn run_custom_path<P: AsRef<Path>>(&self, path: P) -> io::Result<Vec<InstallInfo>> {
let mut cmd = Command::new(path.as_ref());
if self.prerelease {
let _ = cmd.arg("-prerelease");
}
let _ = cmd.arg("-products");
if self.products.is_empty() {
let _ = cmd.arg("*");
} else {
let _ = cmd.args(&self.products);
}
if !self.requires.is_empty() {
let _ = cmd.arg("-requires").args(&self.requires);
}
if self.requires_any {
let _ = cmd.arg("-requiresAny");
}
if let Some(version_range) = self.version.as_ref() {
let _ = cmd.args(&[
"-version",
&format!("[{},{})", version_range.start, version_range.end),
]);
}
if self.latest {
let _ = cmd.arg("-latest");
}
cmd.args(&["-format", "json", "-utf8"])
.output()
.map(|output| {
assert!(output.status.success());
let json = str::from_utf8(&output.stdout).expect("vswhere returned invalid UTF-8");
serde_json::from_str(json).expect("vswhere returned invalid JSON")
})
}
}
impl Default for Config {
fn default() -> Self {
Self::new()
}
}
impl InstallInfo {
pub fn instance_id(&self) -> &str {
&self.instance_id
}
pub fn install_date(&self) -> &DateTime<Utc> {
&self.install_date
}
pub fn installation_name(&self) -> &str {
&self.installation_name
}
pub fn installation_path(&self) -> &Path {
&self.installation_path
}
pub fn installation_version(&self) -> &FourPointVersion {
&self.installation_version
}
pub fn product_id(&self) -> &str {
&self.product_id
}
pub fn product_path(&self) -> &Path {
&self.product_path
}
pub fn is_prerelease(&self) -> bool {
self.is_prerelease
}
pub fn display_name(&self) -> &str {
&self.display_name
}
pub fn description(&self) -> &str {
&self.description
}
pub fn channel_id(&self) -> &str {
&self.channel_id
}
pub fn channel_path(&self) -> &Path {
&self.channel_path
}
pub fn channel_url(&self) -> &Url {
&self.channel_uri
}
pub fn engine_path(&self) -> &Path {
&self.engine_path
}
pub fn release_notes(&self) -> &Url {
&self.release_notes
}
pub fn third_party_notices(&self) -> &Url {
&self.third_party_notices
}
pub fn update_date(&self) -> &DateTime<Utc> {
&self.update_date
}
pub fn catalog(&self) -> &InstallCatalog {
&self.catalog
}
pub fn properties(&self) -> &InstallProperties {
&self.properties
}
}
impl InstallCatalog {
pub fn build_branch(&self) -> &str {
&self.build_branch
}
pub fn build_version(&self) -> &FourPointVersion {
&self.build_version
}
pub fn id(&self) -> &str {
&self.id
}
pub fn local_build(&self) -> &str {
&self.local_build
}
pub fn manifest_name(&self) -> &str {
&self.manifest_name
}
pub fn manifest_type(&self) -> &str {
&self.manifest_type
}
pub fn product_display_version(&self) -> &str {
&self.product_display_version
}
pub fn product_line(&self) -> &str {
&self.product_line
}
pub fn product_line_version(&self) -> &str {
&self.product_line_version
}
pub fn product_milestone(&self) -> &str {
&self.product_milestone
}
pub fn product_milestone_is_pre_release(&self) -> bool {
unimplemented!()
}
pub fn product_name(&self) -> &str {
&self.product_name
}
pub fn product_patch_version(&self) -> &str {
&self.product_patch_version
}
pub fn product_pre_release_milestone_suffix(&self) -> &str {
&self.product_pre_release_milestone_suffix
}
pub fn product_release(&self) -> &str {
&self.product_release
}
pub fn product_semantic_version(&self) -> &Version {
&self.product_semantic_version
}
pub fn required_engine_version(&self) -> &FourPointVersion {
&self.required_engine_version
}
}
impl InstallProperties {
pub fn campaign_id(&self) -> &str {
&self.campaign_id
}
pub fn channel_manifest_id(&self) -> &str {
&self.channel_manifest_id
}
pub fn nickname(&self) -> &str {
&self.nickname
}
pub fn setup_engine_file_path(&self) -> &Path {
&self.setup_engine_file_path
}
}
#[cfg(test)]
mod tests {
use {Config, FourPointVersion};
#[test]
fn test_default() {
let _ = Config::default().run_default_path().expect("failed");
}
#[test]
fn test_args() {
let _ = Config::new()
.find_prerelease_versions(true)
.whitelist_product_id("*")
.whitelist_component_id("Microsoft.VisualStudio.Component.VC.Tools.x86.x64")
.require_any_component(true)
.version_number_range(
FourPointVersion::new(
u16::min_value(),
u16::min_value(),
u16::min_value(),
u16::min_value(),
)
..FourPointVersion::new(
u16::max_value(),
u16::max_value(),
u16::max_value(),
u16::max_value(),
),
)
.only_latest_versions(true)
.run_default_path()
.expect("failed");
}
#[test]
fn test_fake_product() {
let _ = Config::new()
.whitelist_product_id("The quick brown fox jumps over the lazy dog.")
.run_default_path()
.expect("failed");
}
}