1use serde::{Deserialize, Serialize};
5use tracing::warn;
6
7pub const AT_NIX_PREFIX: &str = "@nix ";
9
10#[derive(
12 Clone,
13 Debug,
14 Eq,
15 PartialEq,
16 Serialize,
17 Deserialize,
18 num_enum::TryFromPrimitive,
19 num_enum::IntoPrimitive,
20 Default,
21)]
22#[serde(try_from = "u64", into = "u64")]
23#[cfg_attr(
24 feature = "daemon",
25 derive(nix_compat_derive::NixDeserialize, nix_compat_derive::NixSerialize),
26 nix(try_from = "u64", into = "u64")
27)]
28#[repr(u64)]
29pub enum VerbosityLevel {
30 #[default]
31 Error = 0,
32 Warn = 1,
33 Notice = 2,
34 Info = 3,
35 Talkative = 4,
36 Chatty = 5,
37 Debug = 6,
38 Vomit = 7,
39}
40
41impl std::fmt::Display for VerbosityLevel {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(
44 f,
45 "{}",
46 match self {
47 VerbosityLevel::Error => "error",
48 VerbosityLevel::Warn => "warn",
49 VerbosityLevel::Notice => "notice",
50 VerbosityLevel::Info => "info",
51 VerbosityLevel::Talkative => "talkative",
52 VerbosityLevel::Chatty => "chatty",
53 VerbosityLevel::Debug => "debug",
54 VerbosityLevel::Vomit => "vomit",
55 }
56 )
57 }
58}
59
60#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
63#[serde(tag = "action" )]
64pub enum LogMessage<'a> {
66 #[serde(rename = "start")]
67 Start {
68 #[serde(skip_serializing_if = "Option::is_none")]
69 fields: Option<Vec<Field<'a>>>,
70 id: u64,
71 level: VerbosityLevel,
72 parent: u64,
73 text: std::borrow::Cow<'a, str>,
74 r#type: ActivityType,
75 },
76
77 #[serde(rename = "stop")]
78 Stop { id: u64 },
79
80 #[serde(rename = "result")]
81 Result {
82 fields: Vec<Field<'a>>,
83 id: u64,
84 r#type: ResultType,
85 },
86
87 #[serde(rename = "msg")]
90 Msg {
91 level: VerbosityLevel,
92 msg: std::borrow::Cow<'a, str>,
93 },
94
95 #[serde(rename = "setPhase")]
98 SetPhase { phase: &'a str },
99}
100
101fn serialize_bytes_as_string<S>(b: &[u8], serializer: S) -> Result<S::Ok, S::Error>
102where
103 S: serde::Serializer,
104{
105 match std::str::from_utf8(b) {
106 Ok(s) => serializer.serialize_str(s),
107 Err(_) => {
108 warn!("encountered invalid utf-8 in JSON");
109 serializer.serialize_bytes(b)
110 }
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
117#[serde(untagged)]
118pub enum Field<'a> {
119 Int(u64),
120 String(#[serde(serialize_with = "serialize_bytes_as_string")] std::borrow::Cow<'a, [u8]>),
121}
122
123#[derive(
124 Clone,
125 Debug,
126 Eq,
127 PartialEq,
128 Serialize,
129 Deserialize,
130 num_enum::TryFromPrimitive,
131 num_enum::IntoPrimitive,
132)]
133#[serde(try_from = "u8", into = "u8")]
134#[repr(u8)]
135pub enum ActivityType {
136 Unknown = 0,
137 CopyPath = 100,
138 FileTransfer = 101,
139 Realise = 102,
140 CopyPaths = 103,
141 Builds = 104,
142 Build = 105,
143 OptimiseStore = 106,
144 VerifyPaths = 107,
145 Substitute = 108,
146 QueryPathInfo = 109,
147 PostBuildHook = 110,
148 BuildWaiting = 111,
149 FetchTree = 112,
150}
151
152impl std::fmt::Display for ActivityType {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "{}",
157 match self {
158 ActivityType::Unknown => "unknown",
159 ActivityType::CopyPath => "copy-path",
160 ActivityType::FileTransfer => "file-transfer",
161 ActivityType::Realise => "realise",
162 ActivityType::CopyPaths => "copy-paths",
163 ActivityType::Builds => "builds",
164 ActivityType::Build => "build",
165 ActivityType::OptimiseStore => "optimise-store",
166 ActivityType::VerifyPaths => "verify-paths",
167 ActivityType::Substitute => "substitute",
168 ActivityType::QueryPathInfo => "query-path-info",
169 ActivityType::PostBuildHook => "post-build-hook",
170 ActivityType::BuildWaiting => "build-waiting",
171 ActivityType::FetchTree => "fetch-tree",
172 }
173 )
174 }
175}
176
177#[derive(
178 Clone,
179 Debug,
180 Eq,
181 PartialEq,
182 Serialize,
183 Deserialize,
184 num_enum::TryFromPrimitive,
185 num_enum::IntoPrimitive,
186)]
187#[serde(try_from = "u8", into = "u8")]
188#[repr(u8)]
189pub enum ResultType {
190 FileLinked = 100,
191 BuildLogLine = 101,
192 UntrustedPath = 102,
193 CorruptedPath = 103,
194 SetPhase = 104,
195 Progress = 105,
196 SetExpected = 106,
197 PostBuildLogLine = 107,
198 FetchStatus = 108,
199}
200
201impl<'a> LogMessage<'a> {
202 pub fn from_json_str(s: &'a str) -> Result<Self, Error> {
204 let s = s.strip_prefix(AT_NIX_PREFIX).ok_or(Error::MissingPrefix)?;
205
206 Ok(serde_json::from_str(s)?)
207 }
208}
209
210impl std::fmt::Display for LogMessage<'_> {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(
213 f,
214 "{AT_NIX_PREFIX}{}",
215 serde_json::to_string(self).expect("Failed to serialize LogMessage")
216 )
217 }
218}
219
220#[derive(Debug, thiserror::Error)]
221pub enum Error {
222 #[error("Missing @nix prefix")]
223 MissingPrefix,
224
225 #[error("Failed to deserialize: {0}")]
226 FailedDeserialize(#[from] serde_json::Error),
227}
228
229#[cfg(test)]
230#[allow(unused_variables)]
233mod test {
234 use super::{ActivityType, Field, LogMessage, ResultType, VerbosityLevel};
235 use rstest::rstest;
236
237 #[test]
238 fn verbosity_level() {
239 assert_eq!(
240 VerbosityLevel::try_from(0).expect("must succeed"),
241 VerbosityLevel::Error
242 );
243 assert_eq!(VerbosityLevel::default(), VerbosityLevel::Error);
244
245 VerbosityLevel::try_from(42).expect_err("must fail parsing");
248 }
249
250 #[rstest]
251 #[case::start(
252 r#"@nix {"action":"start","id":1264799149195466,"level":5,"parent":0,"text":"copying '/nix/store/rfqxfljma55x8ybmyg07crnarvqx62sr-nixpkgs-src/pkgs/development/compilers/llvm/18/llvm/lit-shell-script-runner-set-dyld-library-path.patch' to the store","type":0}"#,
253 LogMessage::Start {
254 fields: None,
255 id: 1264799149195466,
256 level: VerbosityLevel::Chatty,
257 parent: 0,
258 text: "copying '/nix/store/rfqxfljma55x8ybmyg07crnarvqx62sr-nixpkgs-src/pkgs/development/compilers/llvm/18/llvm/lit-shell-script-runner-set-dyld-library-path.patch' to the store".into(),
259 r#type: ActivityType::Unknown,
260 },
261 true
262 )]
263 #[case::stop(
264 r#"@nix {"action":"stop","id":1264799149195466}"#,
265 LogMessage::Stop {
266 id: 1264799149195466,
267 },
268 true
269 )]
270 #[case::start_with_fields(
271 r#"@nix {"action":"start","fields":["/nix/store/j3hy9syhvyqhghb13vk1433h81q50wcc-rust_tvix-store-0.1.0-linked","https://cache.nixos.org"],"id":1289035649646595,"level":4,"parent":0,"text":"querying info about '/nix/store/j3hy9syhvyqhghb13vk1433h81q50wcc-rust_tvix-store-0.1.0-linked' on 'https://cache.nixos.org'","type":109}"#,
272 LogMessage::Start { fields: Some(vec![Field::String(b"/nix/store/j3hy9syhvyqhghb13vk1433h81q50wcc-rust_tvix-store-0.1.0-linked".into()),Field::String(b"https://cache.nixos.org".into())]), id: 1289035649646595, level: VerbosityLevel::Talkative, parent: 0, text: "querying info about '/nix/store/j3hy9syhvyqhghb13vk1433h81q50wcc-rust_tvix-store-0.1.0-linked' on 'https://cache.nixos.org'".into(), r#type: ActivityType::QueryPathInfo },
273 true
274 )]
275 #[case::result(
276 r#"@nix {"action":"result","fields":[0,0,0,0],"id":1289035649646594,"type":105}"#,
277 LogMessage::Result {
278 id: 1289035649646594,
279 fields: vec![Field::Int(0), Field::Int(0), Field::Int(0), Field::Int(0)],
280 r#type: ResultType::Progress
281 },
282 true
283 )]
284 #[case::msg(
285 r#"@nix {"action":"msg","level":3,"msg":" /nix/store/zdxxlb3p1vaq1dgh6vfc7c1c52ry4n2f-rust_opentelemetry-semantic-conventions-0.27.0.drv"}"#,
286 LogMessage::Msg { level: VerbosityLevel::Info, msg: " /nix/store/zdxxlb3p1vaq1dgh6vfc7c1c52ry4n2f-rust_opentelemetry-semantic-conventions-0.27.0.drv".into() },
287 true
288 )]
289 #[case::msg_with_raw_msg(
290 r#"@nix {"action":"msg","column":null,"file":null,"level":0,"line":null,"msg":"\u001b[31;1merror:\u001b[0m interrupted by the user","raw_msg":"interrupted by the user"}"#,
291 LogMessage::Msg {
292 level: VerbosityLevel::Error,
293 msg: "\u{001b}[31;1merror:\u{001b}[0m interrupted by the user".into(),
294 },
295 false
297 )]
298 #[case::result_with_fields_int(
299 r#"@nix {"action":"result","fields":[101,146944],"id":15116785938335501,"type":106}"#,
300 LogMessage::Result { fields: vec![
301 Field::Int(101),
302 Field::Int(146944),
303 ], id: 15116785938335501, r#type: ResultType::SetExpected },
304 true
305 )]
306 #[case::set_phase(
307 r#"@nix {"action":"setPhase","phase":"unpackPhase"}"#,
308 LogMessage::SetPhase {
309 phase: "unpackPhase"
310 },
311 true
312 )]
313 #[case::set_phase_result(
314 r#"@nix {"action":"result","fields":["unpackPhase"],"id":418969764757508,"type":104}"#,
315 LogMessage::Result {
316 fields: vec![Field::String(b"unpackPhase".into())],
317 id: 418969764757508,
318 r#type: ResultType::SetPhase,
319 },
320 true
321 )]
322 fn serialize_deserialize(
323 #[case] input_str: &str,
324 #[case] expected_logmessage: LogMessage,
325 #[case] expected_roundtrip: bool,
326 ) {
327 pretty_assertions::assert_matches!(
328 LogMessage::from_json_str(input_str),
329 expected_logmessage,
330 "Expected from_str to return the expected LogMessage"
331 );
332
333 if expected_roundtrip {
334 assert_eq!(
335 input_str,
336 expected_logmessage.to_string(),
337 "Expected LogMessage to roundtrip to input_str"
338 );
339 }
340 }
341}