about summary refs log tree commit diff
path: root/pkgs/build-support/node/fetch-npm-deps/src/cacache.rs
blob: c49c094b85c68a4a7a59a3d492fc62bce162b52d (plain) (blame)
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
use base64::prelude::{Engine, BASE64_STANDARD};
use digest::{Digest, Update};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use std::{
    fmt::Write as FmtWrite,
    fs::{self, File},
    io::Write,
    path::PathBuf,
};
use url::Url;

#[derive(Serialize, Deserialize)]
pub(super) struct Key {
    pub(super) key: String,
    pub(super) integrity: String,
    pub(super) time: u8,
    pub(super) size: usize,
    pub(super) metadata: Metadata,
}

#[derive(Serialize, Deserialize)]
pub(super) struct Metadata {
    pub(super) url: Url,
    pub(super) options: Options,
}

#[derive(Serialize, Deserialize)]
pub(super) struct Options {
    pub(super) compress: bool,
}

pub struct Cache(PathBuf);

fn push_hash_segments(path: &mut PathBuf, hash: &str) {
    path.push(&hash[0..2]);
    path.push(&hash[2..4]);
    path.push(&hash[4..]);
}

impl Cache {
    pub fn new(path: PathBuf) -> Cache {
        Cache(path)
    }

    pub fn init(&self) -> anyhow::Result<()> {
        fs::create_dir_all(self.0.join("content-v2"))?;
        fs::create_dir_all(self.0.join("index-v5"))?;

        Ok(())
    }

    pub fn put(
        &self,
        key: String,
        url: Url,
        data: &[u8],
        integrity: Option<String>,
    ) -> anyhow::Result<()> {
        let (algo, hash, integrity) = if let Some(integrity) = integrity {
            let (algo, hash) = integrity.split_once('-').unwrap();

            (algo.to_string(), BASE64_STANDARD.decode(hash)?, integrity)
        } else {
            let hash = Sha512::new().chain(data).finalize();

            (
                String::from("sha512"),
                hash.to_vec(),
                format!("sha512-{}", BASE64_STANDARD.encode(hash)),
            )
        };

        let content_path = {
            let mut p = self.0.join("content-v2");

            p.push(algo);

            push_hash_segments(
                &mut p,
                &hash.into_iter().fold(String::new(), |mut out, n| {
                    let _ = write!(out, "{n:02x}");
                    out
                }),
            );

            p
        };

        fs::create_dir_all(content_path.parent().unwrap())?;

        fs::write(content_path, data)?;

        let index_path = {
            let mut p = self.0.join("index-v5");

            push_hash_segments(
                &mut p,
                &format!("{:x}", Sha256::new().chain(&key).finalize()),
            );

            p
        };

        fs::create_dir_all(index_path.parent().unwrap())?;

        let data = serde_json::to_string(&Key {
            key,
            integrity,
            time: 0,
            size: data.len(),
            metadata: Metadata {
                url,
                options: Options { compress: true },
            },
        })?;

        let mut file = File::options().append(true).create(true).open(index_path)?;

        write!(file, "{:x}\t{data}", Sha1::new().chain(&data).finalize())?;

        Ok(())
    }
}