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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
/*
 * SPDX-FileCopyrightText: 2021 - 2023  StorPool <support@storpool.com>
 * SPDX-License-Identifier: BSD-2-Clause
 */
//! Detect the OS distribution and version.

#![warn(missing_docs)]
// We do not want to expose the whole of the autogenerated data module.
#![allow(clippy::pub_use)]

use std::clone::Clone;
use std::collections::HashMap;
use std::fs;
use std::io::{Error as IoError, ErrorKind};

use regex::RegexBuilder;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use yai::YAIError;

mod data;

pub mod yai;

#[cfg(test)]
pub mod tests;

pub use data::VariantKind;

/// An error that occurred while determining the Linux variant.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum VariantError {
    /// An invalid variant name was specified.
    #[error("Unknown variant '{0}'")]
    BadVariant(String),

    /// A file to be examined could not be read.
    #[error("Checking for {0}: could not read {1}")]
    FileRead(String, String, #[source] IoError),

    /// Unexpected error parsing the /etc/os-release file.
    #[error("Could not parse the /etc/os-release file")]
    OsRelease(#[source] YAIError),

    /// None of the variants matched.
    #[error("Could not detect the current host's build variant")]
    UnknownVariant,

    /// Something went really, really wrong.
    #[error("Internal sp-variant error: {0}")]
    Internal(String),
}

/// The version of the variant definition format data.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VariantFormatVersion {
    /// The version major number.
    pub major: u32,
    /// The version minor number.
    pub minor: u32,
}

/// The internal format of the variant definition format data.
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[non_exhaustive]
pub struct VariantFormat {
    /// The version of the metadata format.
    pub version: VariantFormatVersion,
}

#[derive(Debug, Serialize, Deserialize)]
struct VariantFormatTop {
    format: VariantFormat,
}

/// Check whether this host is running this particular OS variant.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Detect {
    /// The name of the file to read.
    pub filename: String,
    /// The regular expression pattern to look for in the file.
    pub regex: String,
    /// The "ID" field in the /etc/os-release file.
    pub os_id: String,
    /// The regular expression pattern for the "VERSION_ID" os-release field.
    pub os_version_regex: String,
}

/// The aspects of the StorPool operation supported for this build variant.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Supported {
    /// Is there a StorPool third-party packages repository?
    pub repo: bool,
}

/// Debian package repository data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct DebRepo {
    /// The distribution codename (e.g. "buster").
    pub codename: String,
    /// The distribution vendor ("debian", "ubuntu", etc.).
    pub vendor: String,
    /// The APT sources list file to copy to /etc/apt/sources.list.d/.
    pub sources: String,
    /// The GnuPG keyring file to copy to /usr/share/keyrings/.
    pub keyring: String,
    /// OS packages that need to be installed before `apt-get update` is run.
    pub req_packages: Vec<String>,
}

/// Yum/DNF package repository data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct YumRepo {
    /// The *.repo file to copy to /etc/yum.repos.d/.
    pub yumdef: String,
    /// The keyring file to copy to /etc/pki/rpm-gpg/.
    pub keyring: String,
}

/// OS package repository data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum Repo {
    /// Debian/Ubuntu repository data.
    Deb(DebRepo),
    /// CentOS/Oracle repository data.
    Yum(YumRepo),
}

/// StorPool builder data.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Builder {
    /// The builder name.
    pub alias: String,
    /// The base Docker image that the builder is generated from.
    pub base_image: String,
    /// The branch used by the sp-pkg tool to specify the variant.
    pub branch: String,
    /// The base kernel OS package.
    pub kernel_package: String,
    /// The name of the locale to use for clean UTF-8 output.
    pub utf8_locale: String,
}

/// A single StorPool build variant with all its options.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Variant {
    /// Which variant is that?
    #[serde(rename = "name")]
    pub kind: VariantKind,
    /// The human-readable description of the variant.
    pub descr: String,
    /// The OS "family" that this distribution belongs to.
    pub family: String,
    /// The name of the variant that this one is based on.
    pub parent: String,
    /// The ways to check whether we are running this variant.
    pub detect: Detect,
    /// The aspects of StorPool operation supported for this build variant.
    pub supported: Supported,
    /// The OS commands to execute for particular purposes.
    pub commands: HashMap<String, HashMap<String, Vec<String>>>,
    /// The minimum Python version that we can depend on.
    pub min_sys_python: String,
    /// The StorPool repository files to install.
    pub repo: Repo,
    /// The names of the packages to be used for this variant.
    pub package: HashMap<String, String>,
    /// The name of the directory to install systemd unit files to.
    pub systemd_lib: String,
    /// The filename extension of the OS packages ("deb", "rpm", etc.).
    pub file_ext: String,
    /// The type of initramfs-generating tools.
    pub initramfs_flavor: String,
    /// The data specific to the StorPool builder containers.
    pub builder: Builder,
}

/// The internal variant format data: all build variants, some more info.
#[derive(Debug, Serialize, Deserialize)]
pub struct VariantDefTop {
    format: VariantFormat,
    order: Vec<VariantKind>,
    variants: HashMap<VariantKind, Variant>,
    version: String,
}

/// Get the list of StorPool variants from the internal `data` module.
#[inline]
#[must_use]
pub fn build_variants() -> &'static VariantDefTop {
    data::get_variants()
}

/// Detect the variant that this host is currently running.
///
/// # Errors
/// Propagates any errors from [`detect_from()`].
#[inline]
pub fn detect() -> Result<Variant, VariantError> {
    detect_from(build_variants()).map(Clone::clone)
}

/// Detect the current host's variant from the supplied data.
///
/// # Errors
/// May return a [`VariantError`], either "unknown variant" or a wrapper around
/// an underlying error condition:
/// - any `os-release` parse errors from [`crate::yai::parse()`] other than "file not found"
/// - I/O errors from reading the distribution-specific version files (e.g. `/etc/redhat-release`)
#[allow(clippy::missing_inline_in_public_items)]
pub fn detect_from(variants: &VariantDefTop) -> Result<&Variant, VariantError> {
    match yai::parse("/etc/os-release") {
        Ok(data) => {
            if let Some(os_id) = data.get("ID") {
                if let Some(version_id) = data.get("VERSION_ID") {
                    for kind in &variants.order {
                        let var = &variants.variants.get(kind).ok_or_else(|| {
                            VariantError::Internal(format!(
                                "Internal error: unknown variant {kind} in the order",
                                kind = kind.as_ref()
                            ))
                        })?;
                        if var.detect.os_id != *os_id {
                            continue;
                        }
                        let re_ver = RegexBuilder::new(&var.detect.os_version_regex)
                            .ignore_whitespace(true)
                            .build()
                            .map_err(|err| {
                                VariantError::Internal(format!(
                                    "Internal error: {kind}: could not parse '{regex}': {err}",
                                    kind = kind.as_ref(),
                                    regex = var.detect.regex
                                ))
                            })?;
                        if re_ver.is_match(version_id) {
                            return Ok(var);
                        }
                    }
                }
            }
            // Fall through to the PRETTY_NAME processing.
        }
        Err(YAIError::FileRead(io_err)) if io_err.kind() == ErrorKind::NotFound => (),
        Err(err) => return Err(VariantError::OsRelease(err)),
    }

    for kind in &variants.order {
        let var = &variants.variants.get(kind).ok_or_else(|| {
            VariantError::Internal(format!(
                "Internal error: unknown variant {kind} in the order",
                kind = kind.as_ref()
            ))
        })?;
        let re_line = RegexBuilder::new(&var.detect.regex)
            .ignore_whitespace(true)
            .build()
            .map_err(|err| {
                VariantError::Internal(format!(
                    "Internal error: {kind}: could not parse '{regex}': {err}",
                    kind = kind.as_ref(),
                    regex = var.detect.regex
                ))
            })?;
        match fs::read(&var.detect.filename) {
            Ok(file_bytes) => {
                if let Ok(contents) = String::from_utf8(file_bytes) {
                    {
                        if contents.lines().any(|line| re_line.is_match(line)) {
                            return Ok(var);
                        }
                    }
                }
            }
            Err(err) => {
                if err.kind() != ErrorKind::NotFound {
                    return Err(VariantError::FileRead(
                        var.kind.as_ref().to_owned(),
                        var.detect.filename.clone(),
                        err,
                    ));
                }
            }
        };
    }
    Err(VariantError::UnknownVariant)
}

/// Get the variant with the specified name from the supplied data.
///
/// # Errors
/// - [`VariantKind`] name parse errors, e.g. invalid name
/// - an internal error if there is no data about a recognized variant name
#[inline]
pub fn get_from<'defs>(
    variants: &'defs VariantDefTop,
    name: &str,
) -> Result<&'defs Variant, VariantError> {
    let kind: VariantKind = name.parse()?;
    variants
        .variants
        .get(&kind)
        .ok_or_else(|| VariantError::Internal(format!("No data for the {name} variant")))
}

/// Get the variant with the specified builder alias from the supplied data.
///
/// # Errors
/// May fail if the argument does not specify a recognized variant builder alias.
#[inline]
pub fn get_by_alias_from<'defs>(
    variants: &'defs VariantDefTop,
    alias: &str,
) -> Result<&'defs Variant, VariantError> {
    variants
        .variants
        .values()
        .find(|var| var.builder.alias == alias)
        .ok_or_else(|| VariantError::Internal(format!("No variant with the {alias} alias")))
}

/// Get information about all variants.
#[inline]
#[must_use]
pub fn get_all_variants() -> &'static HashMap<VariantKind, Variant> {
    get_all_variants_from(build_variants())
}

/// Get information about all variants defined in the specified structure.
#[inline]
#[must_use]
pub const fn get_all_variants_from(variants: &VariantDefTop) -> &HashMap<VariantKind, Variant> {
    &variants.variants
}

/// Get information about all variants in the order of inheritance between them.
#[inline]
pub fn get_all_variants_in_order() -> impl Iterator<Item = &'static Variant> {
    get_all_variants_in_order_from(build_variants())
}

/// Get information about all variants defined in the specified structure in order.
///
/// # Panics
/// May panic if the variants data is inconsistent and the variants order array
/// includes a [`VariantKind`] that is not present in the actual hashmap.
/// This should hopefully never ever happen, and there is a unit test for that.
#[inline]
#[allow(clippy::indexing_slicing)]
pub fn get_all_variants_in_order_from(variants: &VariantDefTop) -> impl Iterator<Item = &Variant> {
    variants.order.iter().map(|kind| &variants.variants[kind])
}

/// Get the metadata format version of the variant data.
#[inline]
#[must_use]
pub fn get_format_version() -> (u32, u32) {
    get_format_version_from(build_variants())
}

/// Get the metadata format version of the supplied variant data structure.
#[inline]
#[must_use]
pub const fn get_format_version_from(variants: &VariantDefTop) -> (u32, u32) {
    (variants.format.version.major, variants.format.version.minor)
}

/// Get the program version from the variant data.
#[inline]
#[must_use]
pub fn get_program_version() -> &'static str {
    get_program_version_from(build_variants())
}

/// Get the program version from the supplied variant data structure.
#[inline]
#[must_use]
pub fn get_program_version_from(variants: &VariantDefTop) -> &str {
    &variants.version
}