Skip to main content

lightswitch_object/
buildid.rs

1use std::fmt;
2use std::fmt::Debug;
3use std::fmt::Display;
4use std::fmt::Formatter;
5use std::str;
6use std::str::FromStr;
7
8use anyhow::Result;
9use data_encoding::HEXLOWER;
10use ring::digest::Digest;
11
12const MIN_BUILD_ID_BYTES: usize = 8;
13
14/// Compact identifier for executable files.
15///
16/// Compact identifier for executable files derived from the first 8 bytes
17/// of the build id. By using this smaller type for object files less memory
18/// is used and also comparison, and other operations are cheaper.
19#[derive(PartialEq, Eq, Debug, Clone, Copy, Hash)]
20pub struct ExecutableId(pub u64);
21
22impl Display for ExecutableId {
23    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
24        write!(f, "{:x}", self.0)
25    }
26}
27
28impl From<ExecutableId> for u64 {
29    fn from(executable_id: ExecutableId) -> Self {
30        executable_id.0
31    }
32}
33
34#[derive(Debug, thiserror::Error, Eq, PartialEq)]
35pub enum BuildIdError {
36    #[error("expected at least 8 bytes")]
37    TooSmall,
38}
39
40#[derive(Debug, thiserror::Error)]
41pub enum ParseBuildIdError {
42    #[error("wrong length, must be even")]
43    NotEven,
44    #[error("parsing error")]
45    Parse,
46    #[error("did not fit in the given type")]
47    Fit,
48}
49
50impl FromStr for ExecutableId {
51    type Err = ParseBuildIdError;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        let id = u64::from_str_radix(s, 16).map_err(|_| ParseBuildIdError::Parse)?;
55        Ok(ExecutableId(id))
56    }
57}
58
59#[derive(Hash, Eq, PartialEq, Clone)]
60pub enum BuildIdFlavour {
61    Gnu,
62    Go,
63    Sha256,
64}
65
66/// Represents a build id, which could be either a GNU build ID, the build
67/// ID from Go, or a Sha256 hash of the code in the .text section.
68#[derive(Hash, Eq, PartialEq, Clone)]
69pub struct BuildId {
70    pub flavour: BuildIdFlavour,
71    pub data: Vec<u8>,
72}
73
74impl BuildId {
75    pub fn gnu_from_bytes(bytes: &[u8]) -> Result<Self, BuildIdError> {
76        if bytes.len() < MIN_BUILD_ID_BYTES {
77            return Err(BuildIdError::TooSmall);
78        }
79
80        Ok(BuildId {
81            flavour: BuildIdFlavour::Gnu,
82            data: bytes.to_vec(),
83        })
84    }
85
86    pub fn go_from_bytes(bytes: &[u8]) -> Result<Self, BuildIdError> {
87        if bytes.len() < MIN_BUILD_ID_BYTES {
88            return Err(BuildIdError::TooSmall);
89        }
90
91        Ok(BuildId {
92            flavour: BuildIdFlavour::Go,
93            data: bytes.to_vec(),
94        })
95    }
96
97    pub fn sha256_from_digest(digest: &Digest) -> Result<Self, BuildIdError> {
98        Ok(BuildId {
99            flavour: BuildIdFlavour::Sha256,
100            data: digest.as_ref().to_vec(),
101        })
102    }
103
104    /// Returns an identifier for the executable using the first 8 bytes of the
105    /// build id.
106    pub fn id(&self) -> Result<ExecutableId> {
107        // We want to interpret these bytes as big endian to have its hexadecimal
108        // representation match.
109        Ok(ExecutableId(u64::from_be_bytes(self.data[..8].try_into()?)))
110    }
111
112    pub fn short(&self) -> String {
113        match self.flavour {
114            BuildIdFlavour::Gnu => {
115                self.data
116                    .iter()
117                    .fold(String::with_capacity(self.data.len() * 2), |mut res, el| {
118                        res.push_str(&format!("{el:02x}"));
119                        res
120                    })
121            }
122            BuildIdFlavour::Go => {
123                match str::from_utf8(&self.data) {
124                    Ok(res) => res.to_string(),
125                    // This should never happen in practice.
126                    Err(e) => format!("error converting go build id: {e}"),
127                }
128            }
129            BuildIdFlavour::Sha256 => HEXLOWER.encode(self.data.as_ref()),
130        }
131    }
132
133    pub fn formatted(&self) -> String {
134        format!("{}-{}", self.flavour, self.short())
135    }
136}
137
138impl Display for BuildIdFlavour {
139    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
140        let name = match self {
141            BuildIdFlavour::Gnu => "gnu",
142            BuildIdFlavour::Go => "go",
143            BuildIdFlavour::Sha256 => "sha256",
144        };
145
146        write!(f, "{name}")
147    }
148}
149
150impl Display for BuildId {
151    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
152        write!(f, "{}", self.formatted())
153    }
154}
155
156impl Debug for BuildId {
157    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
158        write!(f, "BuildId({})", self.formatted())
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use ring::digest::{Context, SHA256};
166
167    #[test]
168    fn test_executable_id() {
169        assert_eq!(
170            ExecutableId(0x1020304050607080).to_string(),
171            "1020304050607080"
172        );
173
174        assert_eq!(
175            ExecutableId::from_str("1020304050607080")
176                .unwrap()
177                .to_string(),
178            "1020304050607080"
179        );
180    }
181
182    #[test]
183    fn test_buildid() {
184        assert_eq!(
185            BuildId::gnu_from_bytes(&[0xbe]),
186            Err(BuildIdError::TooSmall)
187        );
188
189        let gnu =
190            BuildId::gnu_from_bytes(&[0xbe, 0xef, 0xca, 0xfe, 0x01, 0x23, 0x45, 0x67]).unwrap();
191        assert_eq!(gnu.to_string(), "gnu-beefcafe01234567");
192
193        gnu.id().unwrap();
194
195        assert_eq!(
196            BuildId::go_from_bytes("fake1234567".as_bytes())
197                .unwrap()
198                .to_string(),
199            "go-fake1234567"
200        );
201
202        let mut context = Context::new(&SHA256);
203        context.update(&[0xbe, 0xef, 0xca, 0xfe]);
204        let digest = context.finish();
205        assert_eq!(
206            BuildId::sha256_from_digest(&digest).unwrap().to_string(),
207            "sha256-b80ad5b1508835ca2191ac800f4bb1a5ae1c3e47f13a8f5ed1b1593337ae5af5"
208        );
209
210        assert_eq!(
211            BuildId::sha256_from_digest(&digest)
212                .unwrap()
213                .id()
214                .unwrap()
215                .0,
216            0xb80ad5b1508835ca
217        );
218    }
219}