snops_common/
binaries.rs

1use std::{
2    fmt::Display,
3    io,
4    path::{Path, PathBuf},
5    str::FromStr,
6};
7
8use chrono::DateTime;
9use serde::{Deserialize, Deserializer, Serialize};
10
11use crate::{
12    format::{DataFormat, DataFormatReader, DataReadError},
13    state::{InternedId, NetworkId},
14    util::sha256_file,
15};
16
17/// A BinaryEntry is the location to a binary with an optional shasum
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct BinaryEntry {
20    pub source: BinarySource,
21    #[serde(default)]
22    pub sha256: Option<String>,
23    #[serde(default)]
24    pub size: Option<u64>,
25}
26
27impl BinaryEntry {
28    pub fn with_api_path(
29        &self,
30        network: NetworkId,
31        storage_id: InternedId,
32        binary_id: InternedId,
33    ) -> BinaryEntry {
34        match &self.source {
35            BinarySource::Url(_) => self.clone(),
36            BinarySource::Path(_) => BinaryEntry {
37                source: BinarySource::Path(PathBuf::from(format!(
38                    "/content/storage/{network}/{storage_id}/binaries/{binary_id}"
39                ))),
40                sha256: self.sha256.clone(),
41                size: self.size,
42            },
43        }
44    }
45
46    /// Check if the sha256 is a valid sha256 hash
47    pub fn check_sha256(&self) -> bool {
48        self.sha256
49            .as_ref()
50            .map(|s| s.len() == 32 && s.chars().all(|c| c.is_ascii_hexdigit()))
51            .unwrap_or(false)
52    }
53
54    /// Check if the given file has the same size as the size in the
55    /// BinaryEntry, return the file's size if it does not match
56    pub fn check_file_size(&self, path: &Path) -> Result<Option<u64>, io::Error> {
57        let Some(size) = self.size else {
58            return Ok(None);
59        };
60        Ok((path.metadata()?.len() != size).then_some(size))
61    }
62
63    /// Check if the given file has the same sha256 as the sha256 in the
64    /// BinaryEntry, return the file's sha256 if it does not match
65    pub fn check_file_sha256(&self, path: &PathBuf) -> Result<Option<String>, io::Error> {
66        let Some(sha256) = &self.sha256 else {
67            return Ok(None);
68        };
69        let file_hash = sha256_file(path)?;
70        Ok((&file_hash != sha256).then_some(file_hash))
71    }
72}
73
74impl Display for BinaryEntry {
75    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
76        writeln!(f, "source: {}", self.source)?;
77        writeln!(f, "sha256: {}", self.sha256.as_deref().unwrap_or("not set"))?;
78        writeln!(
79            f,
80            "size: {}",
81            self.size
82                .map(|s| format!("{s} bytes"))
83                .as_deref()
84                .unwrap_or("not set")
85        )?;
86        if let BinarySource::Path(path) = &self.source {
87            if let Ok(time) = path.metadata().and_then(|m| m.modified()) {
88                writeln!(f, "last modified: {}", DateTime::from(time).naive_local())?;
89            }
90        }
91        Ok(())
92    }
93}
94
95#[derive(Debug, Clone)]
96pub enum BinarySource {
97    Url(url::Url),
98    Path(PathBuf),
99}
100
101impl Display for BinarySource {
102    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
103        match self {
104            BinarySource::Url(url) => write!(f, "{}", url),
105            BinarySource::Path(path) => write!(f, "{}", path.display()),
106        }
107    }
108}
109
110impl FromStr for BinarySource {
111    type Err = url::ParseError;
112
113    fn from_str(s: &str) -> Result<Self, Self::Err> {
114        if s.starts_with("http://") || s.starts_with("https://") {
115            Ok(BinarySource::Url(url::Url::parse(s)?))
116        } else {
117            Ok(BinarySource::Path(PathBuf::from(s)))
118        }
119    }
120}
121
122impl Serialize for BinarySource {
123    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: serde::ser::Serializer,
126    {
127        match self {
128            BinarySource::Url(url) => url.to_string().serialize(serializer),
129            BinarySource::Path(path) => path.to_string_lossy().serialize(serializer),
130        }
131    }
132}
133
134impl<'de> Deserialize<'de> for BinarySource {
135    fn deserialize<D>(deserializer: D) -> Result<BinarySource, D::Error>
136    where
137        D: Deserializer<'de>,
138    {
139        String::deserialize(deserializer)?
140            .parse()
141            .map_err(serde::de::Error::custom)
142    }
143}
144
145impl DataFormat for BinaryEntry {
146    type Header = u8;
147    const LATEST_HEADER: Self::Header = 1;
148
149    fn write_data<W: std::io::Write>(
150        &self,
151        writer: &mut W,
152    ) -> Result<usize, crate::format::DataWriteError> {
153        Ok(self.source.to_string().write_data(writer)?
154            + self.sha256.write_data(writer)?
155            + self.size.write_data(writer)?)
156    }
157
158    fn read_data<R: std::io::Read>(
159        reader: &mut R,
160        header: &Self::Header,
161    ) -> Result<Self, crate::format::DataReadError> {
162        if *header != Self::LATEST_HEADER {
163            return Err(DataReadError::unsupported(
164                "BinaryEntry",
165                Self::LATEST_HEADER,
166                *header,
167            ));
168        }
169
170        Ok(BinaryEntry {
171            source: String::read_data(reader, &())?
172                .parse::<BinarySource>()
173                .map_err(|e| DataReadError::Custom(e.to_string()))?,
174            sha256: reader.read_data(&())?,
175            size: reader.read_data(&())?,
176        })
177    }
178}