1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

//! TOML manifests.

use {
    crate::tar::CompressionFormat,
    anyhow::{anyhow, Result},
    std::collections::HashMap,
    tugger_common::http::RemoteContent,
};

/// Represents a toolchain manifest file.
#[derive(Clone, Debug)]
pub struct Manifest {
    pub packages: HashMap<String, Package>,
}

impl Manifest {
    /// Obtain an instance by parsing TOML bytes.
    pub fn from_toml_bytes(data: &[u8]) -> Result<Self> {
        let table = toml::from_slice(data)?;

        Self::from_toml(table)
    }

    pub fn from_toml(table: toml::value::Table) -> Result<Self> {
        let manifest_version = match table
            .get("manifest-version")
            .ok_or_else(|| anyhow!("manifest TOML doesn't have manifest-version key"))?
        {
            toml::Value::String(s) => s,
            _ => return Err(anyhow!("failed to obtain manifest-version from TOML")),
        };

        if manifest_version != "2" {
            return Err(anyhow!(
                "unhandled manifest-version: {}; only version 2 supported",
                manifest_version,
            ));
        }

        let packages = Self::parse_packages(table)?;

        Ok(Self { packages })
    }

    pub fn parse_packages(mut table: toml::value::Table) -> Result<HashMap<String, Package>> {
        let mut result = HashMap::new();

        let pkg_table = match table
            .remove("pkg")
            .ok_or_else(|| anyhow!("manifest TOML doesn't have any [pkg]"))?
        {
            toml::Value::Table(table) => table,
            _ => return Err(anyhow!("manifest TOML doesn't have table [pkg]")),
        };

        for (k, v) in pkg_table {
            if let toml::Value::Table(v) = v {
                result.insert(k.clone(), Package::from_table(v)?);
            }
        }

        Ok(result)
    }

    /// Find a package for a target triple in this manifest.
    pub fn find_package(
        &self,
        package: &str,
        target_triple: &str,
    ) -> Option<(String, ManifestTargetedPackage)> {
        match self.packages.get(package) {
            Some(package) => match &package.target {
                PackageTarget::Wildcard => None,
                PackageTarget::Targeted(targets) => targets
                    .get(target_triple)
                    .map(|target| (package.version.clone(), target.clone())),
            },
            None => None,
        }
    }
}

/// Represents a `[pkg]` entry in a toolchain manifest TOML.
#[derive(Clone, Debug)]
pub struct Package {
    pub version: String,
    pub target: PackageTarget,
}

impl Package {
    pub fn from_table(mut table: toml::value::Table) -> Result<Self> {
        let version = match table
            .remove("version")
            .ok_or_else(|| anyhow!("[pkg] doesn't have version"))?
        {
            toml::Value::String(v) => v,
            _ => return Err(anyhow!("pkg TOML has non-string version")),
        };

        let mut target_table = match table
            .remove("target")
            .ok_or_else(|| anyhow!("[pkg] does not have .target table"))?
        {
            toml::Value::Table(t) => t,
            _ => return Err(anyhow!("[pkg.target] is not a table")),
        };

        let target = if let Some(toml::Value::Table(_)) = target_table.remove("*") {
            PackageTarget::Wildcard
        } else {
            let mut targets = HashMap::new();

            for (k, v) in target_table {
                if let toml::Value::Table(mut v) = v {
                    let available = v
                        .remove("available")
                        .ok_or_else(|| anyhow!("available not set"))?
                        .as_bool()
                        .ok_or_else(|| anyhow!("available not a bool"))?;

                    let mut urls = vec![];

                    for prefix in &["zst_", "xz_", ""] {
                        let url = v.remove(&format!("{}url", prefix));
                        let hash = v.remove(&format!("{}hash", prefix));

                        if let (Some(url), Some(hash)) = (url, hash) {
                            let url = url
                                .as_str()
                                .ok_or_else(|| anyhow!("url not a string"))?
                                .to_string();
                            let hash = hash
                                .as_str()
                                .ok_or_else(|| anyhow!("hash not a string"))?
                                .to_string();

                            urls.push((
                                match *prefix {
                                    "zst_" => CompressionFormat::Zstd,
                                    "xz_" => CompressionFormat::Xz,
                                    "" => CompressionFormat::Gzip,
                                    _ => panic!("logic error in compression format handling"),
                                },
                                url,
                                hash,
                            ));
                        }
                    }

                    targets.insert(k, ManifestTargetedPackage { available, urls });
                }
            }

            PackageTarget::Targeted(targets)
        };

        Ok(Self { version, target })
    }
}

#[derive(Clone, Debug)]
pub enum PackageTarget {
    Wildcard,
    Targeted(HashMap<String, ManifestTargetedPackage>),
}

#[derive(Clone, Debug)]
pub struct ManifestTargetedPackage {
    pub available: bool,
    pub urls: Vec<(CompressionFormat, String, String)>,
}

impl ManifestTargetedPackage {
    pub fn download_info(&self) -> Option<(CompressionFormat, RemoteContent)> {
        let (format, url, digest) = self.urls.get(0)?;

        Some((
            *format,
            RemoteContent {
                url: url.to_string(),
                sha256: digest.to_string(),
            },
        ))
    }
}