lightswitch_object/
buildid.rs1use 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#[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#[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 pub fn id(&self) -> Result<ExecutableId> {
107 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 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}