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
//  PACKAGES.rs
//    by Lut99
//
//  Created:
//    06 Dec 2022, 11:57:11
//  Last edited:
//    10 Mar 2023, 16:49:17
//  Auto updated?
//    Yes
//
//  Description:
//!   Implements subcommands relating to packages.
//

use std::borrow::Cow;
use std::ffi::OsString;
use std::fs::{self, DirEntry, ReadDir};
use std::path::PathBuf;
use std::str::FromStr;

use brane_cfg::info::Info as _;
use brane_cfg::node::{NodeConfig, NodeKind, NodeSpecificConfig};
use brane_tsk::docker;
use log::{debug, info, warn};
use specifications::version::Version;

pub use crate::errors::PackagesError as Error;


/***** LIBRARY *****/
/// Attempts to hash the given container for use in policies.
///
/// # Arguments
/// - `node_config_path`: The path to the node config file that contains environment settings for this node.
///
/// # Returns
/// Nothing directly, but does print the hash to `stdout`.
///
/// # Errors
/// This function errors if we failed to find the given image or if we failed to hash the file.
pub async fn hash(node_config_path: impl Into<PathBuf>, image: impl Into<String>) -> Result<(), Error> {
    let node_config_path: PathBuf = node_config_path.into();
    let image: String = image.into();
    info!("Computing hash for image '{}'...", image);

    // Load the node config file
    debug!("Loading node config file '{}'...", node_config_path.display());
    let node_config: NodeConfig = match NodeConfig::from_path(&node_config_path) {
        Ok(config) => config,
        Err(err) => {
            return Err(Error::NodeConfigLoadError { err });
        },
    };
    let packages_path: PathBuf = match node_config.node {
        NodeSpecificConfig::Central(node) => node.paths.packages,
        NodeSpecificConfig::Worker(node) => node.paths.packages,
        NodeSpecificConfig::Proxy(_) => return Err(Error::UnsupportedNode { what: "compute a package hash", kind: NodeKind::Proxy }),
    };

    // Attempt to resolve the image
    debug!("Resolving image...");
    let mut image_path: PathBuf = PathBuf::from(&image);
    if image_path.exists() {
        if !image_path.is_file() {
            return Err(Error::FileNotAFile { path: image_path });
        }
    } else {
        // It needs more work

        // Split the image into a name and possible version number
        let (name, version): (String, Version) = match Version::from_package_pair(&image) {
            Ok(res) => res,
            Err(err) => {
                return Err(Error::IllegalNameVersionPair { raw: image, err });
            },
        };

        // Start reading the packages directory
        let entries: ReadDir = match fs::read_dir(&packages_path) {
            Ok(entries) => entries,
            Err(err) => {
                return Err(Error::DirReadError { what: "packages", path: packages_path, err });
            },
        };
        let mut file: Option<(PathBuf, Version)> = None;
        for (i, entry) in entries.enumerate() {
            // Unwrap the entry
            let entry: DirEntry = match entry {
                Ok(entry) => entry,
                Err(err) => {
                    return Err(Error::DirEntryReadError { what: "packages", entry: i, path: packages_path, err });
                },
            };

            // Attempt to analyse the filename by parsing it as a (name, version) pair
            let entry_name: OsString = entry.file_name();
            let entry_name: Cow<str> = entry_name.to_string_lossy();
            let dash_pos: usize = match entry_name.find('-') {
                Some(pos) => pos,
                None => {
                    warn!("Missing dash ('-') in file '{}' (skipping)", entry.path().display());
                    continue;
                },
            };
            let dot_pos: usize = match entry_name.rfind('.') {
                Some(pos) => pos,
                None => {
                    warn!("Missing extension dot ('.') in file '{}' (skipping)", entry.path().display());
                    continue;
                },
            };
            let ename: &str = &entry_name[..dash_pos];
            let eversion: &str = &entry_name[dash_pos + 1..dot_pos];

            // Attempt to parse the eversion
            let eversion: Version = match Version::from_str(eversion) {
                Ok(eversion) => eversion,
                Err(err) => {
                    warn!("File '{}' has illegal version number '{}': {} (skipping)", entry.path().display(), eversion, err);
                    continue;
                },
            };

            // Check if this package checks out
            if name == ename {
                // Only write it if the version makes sense
                if version.is_latest() {
                    // Check if it's 'latest' too or the highest
                    if eversion.is_latest() || file.is_none() || eversion > file.as_ref().unwrap().1 {
                        let is_latest: bool = eversion.is_latest();
                        file = Some((entry.path(), eversion));
                        if is_latest {
                            break;
                        }
                    }
                } else if version == eversion {
                    // Always accept it and stop searching
                    file = Some((entry.path(), eversion));
                    break;
                }
            }
        }

        // Fail if we didn't find any
        if let Some((path, _)) = file {
            image_path = path;
        } else {
            return Err(Error::UnknownImage { path: packages_path, name, version });
        }
    }

    // With the image resolved, hash it
    debug!("Hashing image '{}'...", image_path.display());
    let hash: String = match docker::hash_container(&image_path).await {
        Ok(hash) => hash,
        Err(err) => {
            return Err(Error::HashError { err });
        },
    };

    // Write it
    println!("{hash}");

    // Done
    Ok(())
}