Skip to main content

opys_bundle/
blob.rs

1//! Blobs: files a manifest carries rather than points at.
2//!
3//! A blob is named by the sha256 of its bytes and by nothing else. The
4//! manifest says *which* bytes ([`opys_core::Source::Blob`]); where they are kept
5//! is the business of whoever holds the manifest — an entry of a bundle once
6//! it is published, and before that a [`BlobSource`] on the building machine.
7
8use std::collections::BTreeMap;
9use std::io::{self, Read};
10use std::path::PathBuf;
11
12use base64::Engine;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15
16/// The id of the blob holding exactly `bytes`.
17pub fn blob_id(bytes: &[u8]) -> String {
18    hex::encode(Sha256::digest(bytes))
19}
20
21/// The id and length of the blob holding what `reader` yields, without
22/// holding it in memory.
23pub fn blob_id_of(mut reader: impl Read) -> io::Result<(String, u64)> {
24    let mut hasher = Sha256::new();
25    let size = io::copy(&mut reader, &mut hasher)?;
26    Ok((hex::encode(hasher.finalize()), size))
27}
28
29/// Where a blob's bytes are before the manifest is published: a file on this
30/// machine, or bytes a plugin produced. This is build-machine state and is
31/// never part of a manifest — which is the whole difference from the `file`
32/// and `bytes` sources it replaces.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(try_from = "BlobSourceWire", into = "BlobSourceWire")]
35pub enum BlobSource {
36    File(PathBuf),
37    Bytes(Vec<u8>),
38}
39
40/// Discriminated by which field is present, like every wire shape here.
41/// `bytes` is base64, since this crosses napi as JSON. A struct of options
42/// rather than an untagged enum, so a wrong shape is told the right ones.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub(crate) struct BlobSourceWire {
46    #[serde(default, skip_serializing_if = "Option::is_none")]
47    file: Option<PathBuf>,
48    #[serde(
49        default,
50        skip_serializing_if = "Option::is_none",
51        with = "base64_bytes"
52    )]
53    bytes: Option<Vec<u8>>,
54}
55
56impl TryFrom<BlobSourceWire> for BlobSource {
57    type Error = &'static str;
58
59    fn try_from(raw: BlobSourceWire) -> Result<Self, Self::Error> {
60        match (raw.file, raw.bytes) {
61            (Some(file), None) => Ok(BlobSource::File(file)),
62            (None, Some(bytes)) => Ok(BlobSource::Bytes(bytes)),
63            _ => Err("a blob is kept as `{ file }` or as `{ bytes }`"),
64        }
65    }
66}
67
68impl From<BlobSource> for BlobSourceWire {
69    fn from(source: BlobSource) -> Self {
70        match source {
71            BlobSource::File(file) => BlobSourceWire {
72                file: Some(file),
73                bytes: None,
74            },
75            BlobSource::Bytes(bytes) => BlobSourceWire {
76                file: None,
77                bytes: Some(bytes),
78            },
79        }
80    }
81}
82
83mod base64_bytes {
84    use super::*;
85    use serde::{Deserializer, Serializer};
86
87    const ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD;
88
89    pub fn serialize<S: Serializer>(
90        bytes: &Option<Vec<u8>>,
91        serializer: S,
92    ) -> Result<S::Ok, S::Error> {
93        match bytes {
94            Some(bytes) => serializer.serialize_str(&ENGINE.encode(bytes)),
95            None => serializer.serialize_none(),
96        }
97    }
98
99    pub fn deserialize<'de, D: Deserializer<'de>>(
100        deserializer: D,
101    ) -> Result<Option<Vec<u8>>, D::Error> {
102        let text = String::deserialize(deserializer)?;
103        ENGINE
104            .decode(text)
105            .map(Some)
106            .map_err(serde::de::Error::custom)
107    }
108}
109
110/// Blob id → where its bytes are. Ordered, because a bundle is written from
111/// it and must not differ between two builds of the same thing.
112pub type Blobs = BTreeMap<String, BlobSource>;