about summary refs log tree commit diff
path: root/tvix/nix-compat
diff options
context:
space:
mode:
authorPicnoir <picnoir@alternativebit.fr>2024-04-05T09·33+0200
committerpicnoir picnoir <picnoir@alternativebit.fr>2024-04-07T20·30+0000
commit199f9b0a79de0fb0fd57ce9307b36390339ee7e7 (patch)
tree806fb4ec29232cd25b0b4c41b072948cd2239679 /tvix/nix-compat
parent289b3126db6248ea7dafddaf9931f4bf277c3d88 (diff)
feat(tvix/nix-compat): read client setting from wire r/7867
Add the primitives necessary to read the client settings from the Nix
daemon wire protocol.

Introducing the read_string primitive. This trivial primitive parses a
read_bytes call, check the bytes are valid utf-8 bytes and wraps the
result in a String.

Change-Id: Ie1253523a6bd4e31e7924e9898a0898109da2fa0
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11358
Reviewed-by: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/nix-compat')
-rw-r--r--tvix/nix-compat/src/wire/bytes.rs20
-rw-r--r--tvix/nix-compat/src/wire/worker_protocol.rs208
2 files changed, 225 insertions, 3 deletions
diff --git a/tvix/nix-compat/src/wire/bytes.rs b/tvix/nix-compat/src/wire/bytes.rs
index 1a25daca8b4f..070f2368fdc3 100644
--- a/tvix/nix-compat/src/wire/bytes.rs
+++ b/tvix/nix-compat/src/wire/bytes.rs
@@ -1,4 +1,7 @@
-use std::ops::RangeBounds;
+use std::{
+    io::{Error, ErrorKind},
+    ops::RangeBounds,
+};
 
 use tokio::io::{AsyncReadExt, AsyncWriteExt};
 
@@ -58,6 +61,21 @@ where
     Ok(buf)
 }
 
+/// Read a Nix daemon string from the AsyncWrite, encoded as utf8.
+/// Rejects reading more than `allowed_size` bytes
+///
+/// A Nix daemon string is made up of two distincts parts:
+/// 1. Its lenght, LE-encoded on 64 bits.
+/// 2. Its content. 0-padded on 64 bits.
+pub async fn read_string<R, S>(r: &mut R, allowed_size: S) -> std::io::Result<String>
+where
+    R: AsyncReadExt + Unpin,
+    S: RangeBounds<u64>,
+{
+    let bytes = read_bytes(r, allowed_size).await?;
+    String::from_utf8(bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))
+}
+
 /// Writes a sequence of sized bits to a (hopefully buffered)
 /// [AsyncWriteExt] handle.
 ///
diff --git a/tvix/nix-compat/src/wire/worker_protocol.rs b/tvix/nix-compat/src/wire/worker_protocol.rs
index ed47aedbca46..77d510425016 100644
--- a/tvix/nix-compat/src/wire/worker_protocol.rs
+++ b/tvix/nix-compat/src/wire/worker_protocol.rs
@@ -1,4 +1,7 @@
-use std::io::{Error, ErrorKind};
+use std::{
+    collections::HashMap,
+    io::{Error, ErrorKind},
+};
 
 use enum_primitive_derive::Primitive;
 use num_traits::{FromPrimitive, ToPrimitive};
@@ -6,8 +9,16 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
 
 use crate::wire::primitive;
 
+use super::bytes::read_string;
+
 pub static STDERR_LAST: u64 = 0x616c7473;
 
+/// Max length of a Nix setting name/value. In bytes.
+///
+/// This value has been arbitrarily choosen after looking the nix.conf
+/// manpage. Don't hesitate to increase it if it's too limiting.
+pub static MAX_SETTING_SIZE: u64 = 1024;
+
 /// Worker Operation
 ///
 /// These operations are encoded as unsigned 64 bits before being sent
@@ -66,11 +77,102 @@ pub enum Operation {
     AddPermRoot = 47,
 }
 
+/// Log verbosity. In the Nix wire protocol, the client requests a
+/// verbosity level to the daemon, which in turns does not produce any
+/// log below this verbosity.
+#[derive(Debug, PartialEq, Primitive)]
+pub enum Verbosity {
+    LvlError = 0,
+    LvlWarn = 1,
+    LvlNotice = 2,
+    LvlInfo = 3,
+    LvlTalkative = 4,
+    LvlChatty = 5,
+    LvlDebug = 6,
+    LvlVomit = 7,
+}
+
+/// Settings requested by the client. These settings are applied to a
+/// connection to between the daemon and a client.
+#[derive(Debug, PartialEq)]
+pub struct ClientSettings {
+    pub keep_failed: bool,
+    pub keep_going: bool,
+    pub try_fallback: bool,
+    pub verbosity: Verbosity,
+    pub max_build_jobs: u64,
+    pub max_silent_time: u64,
+    pub verbose_build: bool,
+    pub build_cores: u64,
+    pub use_substitutes: bool,
+    /// Key/Value dictionary in charge of overriding the settings set
+    /// by the Nix config file.
+    ///
+    /// Some settings can be safely overidden,
+    /// some other require the user running the Nix client to be part
+    /// of the trusted users group.
+    pub overrides: HashMap<String, String>,
+}
+
+/// Reads the client settings from the wire.
+///
+/// Note: this function **only** reads the settings. It does not
+/// manage the log state with the daemon. You'll have to do that on
+/// your own. A minimal log implementation will consist in sending
+/// back [STDERR_LAST] to the client after reading the client
+/// settings.
+///
+/// FUTUREWORK: write serialization.
+pub async fn read_client_settings<R: AsyncReadExt + Unpin>(
+    r: &mut R,
+    client_version: u64,
+) -> std::io::Result<ClientSettings> {
+    let keep_failed = primitive::read_bool(r).await?;
+    let keep_going = primitive::read_bool(r).await?;
+    let try_fallback = primitive::read_bool(r).await?;
+    let verbosity_uint = primitive::read_u64(r).await?;
+    let verbosity = Verbosity::from_u64(verbosity_uint).ok_or_else(|| {
+        Error::new(
+            ErrorKind::InvalidData,
+            format!("Can't convert integer {} to verbosity", verbosity_uint),
+        )
+    })?;
+    let max_build_jobs = primitive::read_u64(r).await?;
+    let max_silent_time = primitive::read_u64(r).await?;
+    _ = primitive::read_u64(r).await?; // obsolete useBuildHook
+    let verbose_build = primitive::read_bool(r).await?;
+    _ = primitive::read_u64(r).await?; // obsolete logType
+    _ = primitive::read_u64(r).await?; // obsolete printBuildTrace
+    let build_cores = primitive::read_u64(r).await?;
+    let use_substitutes = primitive::read_bool(r).await?;
+    let mut overrides = HashMap::new();
+    if client_version >= 12 {
+        let num_overrides = primitive::read_u64(r).await?;
+        for _ in 0..num_overrides {
+            let name = read_string(r, 0..MAX_SETTING_SIZE).await?;
+            let value = read_string(r, 0..MAX_SETTING_SIZE).await?;
+            overrides.insert(name, value);
+        }
+    }
+    Ok(ClientSettings {
+        keep_failed,
+        keep_going,
+        try_fallback,
+        verbosity,
+        max_build_jobs,
+        max_silent_time,
+        verbose_build,
+        build_cores,
+        use_substitutes,
+        overrides,
+    })
+}
+
 /// Read a worker [Operation] from the wire.
 pub async fn read_op<R: AsyncReadExt + Unpin>(r: &mut R) -> std::io::Result<Operation> {
     let op_number = primitive::read_u64(r).await?;
     Operation::from_u64(op_number).ok_or(Error::new(
-        ErrorKind::Other,
+        ErrorKind::InvalidData,
         format!("Invalid OP number {}", op_number),
     ))
 }
@@ -105,3 +207,105 @@ where
         Trust::NotTrusted => primitive::write_u64(conn, 2).await,
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use hex_literal::hex;
+    use tokio_test::io::Builder;
+
+    #[tokio::test]
+    async fn test_read_client_settings_without_overrides() {
+        // Client settings bits captured from a Nix 2.3.17 run w/ sockdump (protocol version 21).
+        let wire_bits = hex!(
+            "00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             02 00 00 00 00 00 00 00 \
+             10 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             01 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             01 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00"
+        );
+        let mut mock = Builder::new().read(&wire_bits).build();
+        let settings = read_client_settings(&mut mock, 21)
+            .await
+            .expect("should parse");
+        let expected = ClientSettings {
+            keep_failed: false,
+            keep_going: false,
+            try_fallback: false,
+            verbosity: Verbosity::LvlNotice,
+            max_build_jobs: 16,
+            max_silent_time: 0,
+            verbose_build: false,
+            build_cores: 0,
+            use_substitutes: true,
+            overrides: HashMap::new(),
+        };
+        assert_eq!(settings, expected);
+    }
+
+    #[tokio::test]
+    async fn test_read_client_settings_with_overrides() {
+        // Client settings bits captured from a Nix 2.3.17 run w/ sockdump (protocol version 21).
+        let wire_bits = hex!(
+            "00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             02 00 00 00 00 00 00 00 \
+             10 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             01 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             00 00 00 00 00 00 00 00 \
+             01 00 00 00 00 00 00 00 \
+             02 00 00 00 00 00 00 00 \
+             0c 00 00 00 00 00 00 00 \
+             61 6c 6c 6f 77 65 64 2d \
+             75 72 69 73 00 00 00 00 \
+             1e 00 00 00 00 00 00 00 \
+             68 74 74 70 73 3a 2f 2f \
+             62 6f 72 64 65 61 75 78 \
+             2e 67 75 69 78 2e 67 6e \
+             75 2e 6f 72 67 2f 00 00 \
+             0d 00 00 00 00 00 00 00 \
+             61 6c 6c 6f 77 65 64 2d \
+             75 73 65 72 73 00 00 00 \
+             0b 00 00 00 00 00 00 00 \
+             6a 65 61 6e 20 70 69 65 \
+             72 72 65 00 00 00 00 00"
+        );
+        let mut mock = Builder::new().read(&wire_bits).build();
+        let settings = read_client_settings(&mut mock, 21)
+            .await
+            .expect("should parse");
+        let overrides = HashMap::from([
+            (
+                String::from("allowed-uris"),
+                String::from("https://bordeaux.guix.gnu.org/"),
+            ),
+            (String::from("allowed-users"), String::from("jean pierre")),
+        ]);
+        let expected = ClientSettings {
+            keep_failed: false,
+            keep_going: false,
+            try_fallback: false,
+            verbosity: Verbosity::LvlNotice,
+            max_build_jobs: 16,
+            max_silent_time: 0,
+            verbose_build: false,
+            build_cores: 0,
+            use_substitutes: true,
+            overrides,
+        };
+        assert_eq!(settings, expected);
+    }
+}