about summary refs log tree commit diff
path: root/tvix/store/src/proto
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/store/src/proto')
-rw-r--r--tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs12
-rw-r--r--tvix/store/src/proto/mod.rs364
-rw-r--r--tvix/store/src/proto/tests/pathinfo.rs489
3 files changed, 309 insertions, 556 deletions
diff --git a/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
index 60da73012df7..5da3b23c269c 100644
--- a/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
+++ b/tvix/store/src/proto/grpc_pathinfoservice_wrapper.rs
@@ -1,5 +1,5 @@
 use crate::nar::{NarCalculationService, RenderError};
-use crate::pathinfoservice::PathInfoService;
+use crate::pathinfoservice::{PathInfo, PathInfoService};
 use crate::proto;
 use futures::{stream::BoxStream, TryStreamExt};
 use std::ops::Deref;
@@ -44,7 +44,7 @@ where
                     .map_err(|_e| Status::invalid_argument("invalid output digest length"))?;
                 match self.path_info_service.get(digest).await {
                     Ok(None) => Err(Status::not_found("PathInfo not found")),
-                    Ok(Some(path_info)) => Ok(Response::new(path_info)),
+                    Ok(Some(path_info)) => Ok(Response::new(proto::PathInfo::from(path_info))),
                     Err(e) => {
                         warn!(err = %e, "failed to get PathInfo");
                         Err(e.into())
@@ -56,12 +56,15 @@ where
 
     #[instrument(skip_all)]
     async fn put(&self, request: Request<proto::PathInfo>) -> Result<Response<proto::PathInfo>> {
-        let path_info = request.into_inner();
+        let path_info_proto = request.into_inner();
+
+        let path_info = PathInfo::try_from(path_info_proto)
+            .map_err(|e| Status::invalid_argument(format!("Invalid path info: {e}")))?;
 
         // Store the PathInfo in the client. Clients MUST validate the data
         // they receive, so we don't validate additionally here.
         match self.path_info_service.put(path_info).await {
-            Ok(path_info_new) => Ok(Response::new(path_info_new)),
+            Ok(path_info_new) => Ok(Response::new(proto::PathInfo::from(path_info_new))),
             Err(e) => {
                 warn!(err = %e, "failed to put PathInfo");
                 Err(e.into())
@@ -99,6 +102,7 @@ where
         let stream = Box::pin(
             self.path_info_service
                 .list()
+                .map_ok(proto::PathInfo::from)
                 .map_err(|e| Status::internal(e.to_string())),
         );
 
diff --git a/tvix/store/src/proto/mod.rs b/tvix/store/src/proto/mod.rs
index f3ea4b196946..807f03854ddc 100644
--- a/tvix/store/src/proto/mod.rs
+++ b/tvix/store/src/proto/mod.rs
@@ -4,7 +4,7 @@ use bytes::Bytes;
 use data_encoding::BASE64;
 // https://github.com/hyperium/tonic/issues/1056
 use nix_compat::{
-    narinfo::Flags,
+    narinfo::{Signature, SignatureError},
     nixhash::{CAHash, NixHash},
     store_path::{self, StorePathRef},
 };
@@ -17,6 +17,8 @@ pub use grpc_pathinfoservice_wrapper::GRPCPathInfoServiceWrapper;
 
 tonic::include_proto!("tvix.store.v1");
 
+use tvix_castore::proto as castorepb;
+
 #[cfg(feature = "tonic-reflection")]
 /// Compiled file descriptors for implementing [gRPC
 /// reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) with e.g.
@@ -70,183 +72,18 @@ pub enum ValidatePathInfoError {
     /// The deriver field is invalid.
     #[error("deriver field is invalid: {0}")]
     InvalidDeriverField(store_path::Error),
-}
 
-/// Parses a root node name.
-///
-/// On success, this returns the parsed [store_path::StorePathRef].
-/// On error, it returns an error generated from the supplied constructor.
-fn parse_node_name_root<E>(
-    name: &[u8],
-    err: fn(Vec<u8>, store_path::Error) -> E,
-) -> Result<store_path::StorePathRef<'_>, E> {
-    store_path::StorePathRef::from_bytes(name).map_err(|e| err(name.to_vec(), e))
-}
+    /// The narinfo field is missing
+    #[error("The narinfo field is missing")]
+    NarInfoFieldMissing,
 
-impl PathInfo {
-    /// validate performs some checks on the PathInfo struct,
-    /// Returning either a [store_path::StorePath] of the root node, or a
-    /// [ValidatePathInfoError].
-    pub fn validate(&self) -> Result<store_path::StorePath<String>, ValidatePathInfoError> {
-        // ensure the references have the right number of bytes.
-        for (i, reference) in self.references.iter().enumerate() {
-            if reference.len() != store_path::DIGEST_SIZE {
-                return Err(ValidatePathInfoError::InvalidReferenceDigestLen(
-                    i,
-                    reference.len(),
-                ));
-            }
-        }
+    /// The ca field is invalid
+    #[error("The ca field is invalid: {0}")]
+    InvalidCaField(ConvertCAError),
 
-        // If there is a narinfo field populated…
-        if let Some(narinfo) = &self.narinfo {
-            // ensure the nar_sha256 digest has the correct length.
-            if narinfo.nar_sha256.len() != 32 {
-                return Err(ValidatePathInfoError::InvalidNarSha256DigestLen(
-                    narinfo.nar_sha256.len(),
-                ));
-            }
-
-            // ensure the number of references there matches PathInfo.references count.
-            if narinfo.reference_names.len() != self.references.len() {
-                return Err(ValidatePathInfoError::InconsistentNumberOfReferences(
-                    self.references.len(),
-                    narinfo.reference_names.len(),
-                ));
-            }
-
-            // parse references in reference_names.
-            for (i, reference_name_str) in narinfo.reference_names.iter().enumerate() {
-                // ensure thy parse as (non-absolute) store path
-                let reference_names_store_path = store_path::StorePathRef::from_bytes(
-                    reference_name_str.as_bytes(),
-                )
-                .map_err(|_| {
-                    ValidatePathInfoError::InvalidNarinfoReferenceName(
-                        i,
-                        reference_name_str.to_owned(),
-                    )
-                })?;
-
-                // ensure their digest matches the one at self.references[i].
-                {
-                    // This is safe, because we ensured the proper length earlier already.
-                    let reference_digest = self.references[i].to_vec().try_into().unwrap();
-
-                    if reference_names_store_path.digest() != &reference_digest {
-                        return Err(
-                            ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
-                                i,
-                                reference_digest,
-                                *reference_names_store_path.digest(),
-                            ),
-                        );
-                    }
-                }
-
-                // If the Deriver field is populated, ensure it parses to a
-                // [store_path::StorePath].
-                // We can't check for it to *not* end with .drv, as the .drv files produced by
-                // recursive Nix end with multiple .drv suffixes, and only one is popped when
-                // converting to this field.
-                if let Some(deriver) = &narinfo.deriver {
-                    store_path::StorePathRef::from_name_and_digest(&deriver.name, &deriver.digest)
-                        .map_err(ValidatePathInfoError::InvalidDeriverField)?;
-                }
-            }
-        }
-
-        // Ensure there is a (root) node present, and it properly parses to a [store_path::StorePath].
-        let root_nix_path = match &self.node {
-            None => Err(ValidatePathInfoError::NoNodePresent)?,
-            Some(node) => {
-                // NOTE: We could have some PathComponent not allocating here,
-                // so this can return StorePathRef.
-                // However, as this will get refactored away to stricter types
-                // soon anyways, there's no point.
-                let (name, _node) = node
-                    .clone()
-                    .into_name_and_node()
-                    .map_err(ValidatePathInfoError::InvalidRootNode)?;
-
-                // parse the name of the node itself and return
-                parse_node_name_root(name.as_ref(), ValidatePathInfoError::InvalidNodeName)?
-                    .to_owned()
-            }
-        };
-
-        // return the root nix path
-        Ok(root_nix_path)
-    }
-
-    /// With self and its store path name, this reconstructs a
-    /// [nix_compat::narinfo::NarInfo<'_>].
-    /// It can be used to validate Signatures, or get back a (sparse) NarInfo
-    /// struct to prepare writing it out.
-    ///
-    /// It assumes self to be validated first, and will only return None if the
-    /// `narinfo` field is unpopulated.
-    ///
-    /// It does very little allocation (a Vec each for `signatures` and
-    /// `references`), the rest points to data owned elsewhere.
-    ///
-    /// Keep in mind this is not able to reconstruct all data present in the
-    /// NarInfo<'_>, as some of it is not stored at all:
-    /// - the `system`, `file_hash` and `file_size` fields are set to `None`.
-    /// - the URL is set to an empty string.
-    /// - Compression is set to "none"
-    ///
-    /// If you want to render it out to a string and be able to parse it back
-    /// in, at least URL *must* be set again.
-    pub fn to_narinfo<'a>(
-        &'a self,
-        store_path: store_path::StorePathRef<'a>,
-    ) -> Option<nix_compat::narinfo::NarInfo<'_>> {
-        let narinfo = &self.narinfo.as_ref()?;
-
-        Some(nix_compat::narinfo::NarInfo {
-            flags: Flags::empty(),
-            store_path,
-            nar_hash: narinfo
-                .nar_sha256
-                .as_ref()
-                .try_into()
-                .expect("invalid narhash"),
-            nar_size: narinfo.nar_size,
-            references: narinfo
-                .reference_names
-                .iter()
-                .map(|ref_name| {
-                    // This shouldn't pass validation
-                    StorePathRef::from_bytes(ref_name.as_bytes()).expect("invalid reference")
-                })
-                .collect(),
-            signatures: narinfo
-                .signatures
-                .iter()
-                .map(|sig| {
-                    nix_compat::narinfo::SignatureRef::new(
-                        &sig.name,
-                        // This shouldn't pass validation
-                        sig.data[..].try_into().expect("invalid signature len"),
-                    )
-                })
-                .collect(),
-            ca: narinfo
-                .ca
-                .as_ref()
-                .map(|ca| ca.try_into().expect("invalid ca")),
-            system: None,
-            deriver: narinfo.deriver.as_ref().map(|deriver| {
-                StorePathRef::from_name_and_digest(&deriver.name, &deriver.digest)
-                    .expect("invalid deriver")
-            }),
-            url: "",
-            compression: Some("none"),
-            file_hash: None,
-            file_size: None,
-        })
-    }
+    /// The signature at position is invalid
+    #[error("The signature at position {0} is invalid: {1}")]
+    InvalidSignature(usize, SignatureError),
 }
 
 /// Errors that can occur when converting from a [nar_info::Ca] to a (stricter)
@@ -341,45 +178,154 @@ impl From<&nix_compat::nixhash::CAHash> for nar_info::Ca {
     }
 }
 
-impl From<&nix_compat::narinfo::NarInfo<'_>> for NarInfo {
-    /// Converts from a NarInfo (returned from the NARInfo parser) to the proto-
-    /// level NarInfo struct.
-    fn from(value: &nix_compat::narinfo::NarInfo<'_>) -> Self {
-        let signatures = value
-            .signatures
-            .iter()
-            .map(|sig| nar_info::Signature {
-                name: sig.name().to_string(),
-                data: Bytes::copy_from_slice(sig.bytes()),
-            })
-            .collect();
-
-        NarInfo {
-            nar_size: value.nar_size,
-            nar_sha256: Bytes::copy_from_slice(&value.nar_hash),
-            signatures,
-            reference_names: value.references.iter().map(|r| r.to_string()).collect(),
-            deriver: value.deriver.as_ref().map(|sp| StorePath {
-                name: (*sp.name()).to_owned(),
-                digest: Bytes::copy_from_slice(sp.digest()),
-            }),
-            ca: value.ca.as_ref().map(|ca| ca.into()),
-        }
-    }
-}
-
-impl From<&nix_compat::narinfo::NarInfo<'_>> for PathInfo {
-    /// Converts from a NarInfo (returned from the NARInfo parser) to a PathInfo
-    /// struct with the node set to None.
-    fn from(value: &nix_compat::narinfo::NarInfo<'_>) -> Self {
+impl From<crate::pathinfoservice::PathInfo> for PathInfo {
+    fn from(value: crate::pathinfoservice::PathInfo) -> Self {
         Self {
-            node: None,
+            node: Some(castorepb::Node::from_name_and_node(
+                value.store_path.to_string().into_bytes().into(),
+                value.node,
+            )),
             references: value
                 .references
                 .iter()
-                .map(|x| Bytes::copy_from_slice(x.digest()))
+                .map(|reference| Bytes::copy_from_slice(reference.digest()))
                 .collect(),
-            narinfo: Some(value.into()),
+            narinfo: Some(NarInfo {
+                nar_size: value.nar_size,
+                nar_sha256: Bytes::copy_from_slice(&value.nar_sha256),
+                signatures: value
+                    .signatures
+                    .iter()
+                    .map(|sig| nar_info::Signature {
+                        name: sig.name().to_string(),
+                        data: Bytes::copy_from_slice(sig.bytes()),
+                    })
+                    .collect(),
+                reference_names: value.references.iter().map(|r| r.to_string()).collect(),
+                deriver: value.deriver.as_ref().map(|sp| StorePath {
+                    name: (*sp.name()).to_owned(),
+                    digest: Bytes::copy_from_slice(sp.digest()),
+                }),
+                ca: value.ca.as_ref().map(|ca| ca.into()),
+            }),
+        }
+    }
+}
+
+impl TryFrom<PathInfo> for crate::pathinfoservice::PathInfo {
+    type Error = ValidatePathInfoError;
+    fn try_from(value: PathInfo) -> Result<Self, Self::Error> {
+        let narinfo = value
+            .narinfo
+            .ok_or_else(|| ValidatePathInfoError::NarInfoFieldMissing)?;
+
+        // ensure the references have the right number of bytes.
+        for (i, reference) in value.references.iter().enumerate() {
+            if reference.len() != store_path::DIGEST_SIZE {
+                return Err(ValidatePathInfoError::InvalidReferenceDigestLen(
+                    i,
+                    reference.len(),
+                ));
+            }
+        }
+
+        // ensure the number of references there matches PathInfo.references count.
+        if narinfo.reference_names.len() != value.references.len() {
+            return Err(ValidatePathInfoError::InconsistentNumberOfReferences(
+                value.references.len(),
+                narinfo.reference_names.len(),
+            ));
+        }
+
+        // parse references in reference_names.
+        let mut references = vec![];
+        for (i, reference_name_str) in narinfo.reference_names.iter().enumerate() {
+            // ensure thy parse as (non-absolute) store path
+            let reference_names_store_path =
+                StorePathRef::from_bytes(reference_name_str.as_bytes()).map_err(|_| {
+                    ValidatePathInfoError::InvalidNarinfoReferenceName(
+                        i,
+                        reference_name_str.to_owned(),
+                    )
+                })?;
+
+            // ensure their digest matches the one at self.references[i].
+            {
+                // This is safe, because we ensured the proper length earlier already.
+                let reference_digest = value.references[i].to_vec().try_into().unwrap();
+
+                if reference_names_store_path.digest() != &reference_digest {
+                    return Err(
+                        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
+                            i,
+                            reference_digest,
+                            *reference_names_store_path.digest(),
+                        ),
+                    );
+                } else {
+                    references.push(reference_names_store_path.to_owned());
+                }
+            }
         }
+
+        let nar_sha256_length = narinfo.nar_sha256.len();
+
+        // split value.node into the name and node components
+        let (name, node) = value
+            .node
+            .ok_or_else(|| ValidatePathInfoError::NoNodePresent)?
+            .into_name_and_node()
+            .map_err(ValidatePathInfoError::InvalidRootNode)?;
+
+        Ok(Self {
+            // value.node has a valid name according to the castore model but might not parse to a
+            // [StorePath]
+            store_path: nix_compat::store_path::StorePath::from_bytes(name.as_ref()).map_err(
+                |err| ValidatePathInfoError::InvalidNodeName(name.as_ref().to_vec(), err),
+            )?,
+            node,
+            references,
+            nar_size: narinfo.nar_size,
+            nar_sha256: narinfo.nar_sha256.to_vec()[..]
+                .try_into()
+                .map_err(|_| ValidatePathInfoError::InvalidNarSha256DigestLen(nar_sha256_length))?,
+            // If the Deriver field is populated, ensure it parses to a
+            // [StorePath].
+            // We can't check for it to *not* end with .drv, as the .drv files produced by
+            // recursive Nix end with multiple .drv suffixes, and only one is popped when
+            // converting to this field.
+            deriver: narinfo
+                .deriver
+                .map(|deriver| {
+                    nix_compat::store_path::StorePath::from_name_and_digest(
+                        &deriver.name,
+                        &deriver.digest,
+                    )
+                    .map_err(ValidatePathInfoError::InvalidDeriverField)
+                })
+                .transpose()?,
+            signatures: narinfo
+                .signatures
+                .into_iter()
+                .enumerate()
+                .map(|(i, signature)| {
+                    signature.data.to_vec()[..]
+                        .try_into()
+                        .map_err(|_| {
+                            ValidatePathInfoError::InvalidSignature(
+                                i,
+                                SignatureError::InvalidSignatureLen(signature.data.len()),
+                            )
+                        })
+                        .map(|signature_data| Signature::new(signature.name, signature_data))
+                })
+                .collect::<Result<Vec<_>, ValidatePathInfoError>>()?,
+            ca: narinfo
+                .ca
+                .as_ref()
+                .map(TryFrom::try_from)
+                .transpose()
+                .map_err(ValidatePathInfoError::InvalidCaField)?,
+        })
     }
 }
diff --git a/tvix/store/src/proto/tests/pathinfo.rs b/tvix/store/src/proto/tests/pathinfo.rs
index f5ecc798bbfb..320f419b6c59 100644
--- a/tvix/store/src/proto/tests/pathinfo.rs
+++ b/tvix/store/src/proto/tests/pathinfo.rs
@@ -1,274 +1,226 @@
-use crate::proto::{nar_info::Signature, NarInfo, PathInfo, ValidatePathInfoError};
-use crate::tests::fixtures::*;
+use crate::pathinfoservice::PathInfo;
+use crate::proto::{self, ValidatePathInfoError};
+use crate::tests::fixtures::{DUMMY_PATH, DUMMY_PATH_DIGEST, DUMMY_PATH_STR};
 use bytes::Bytes;
-use data_encoding::BASE64;
-use nix_compat::nixbase32;
-use nix_compat::store_path::{self, StorePath, StorePathRef};
+use lazy_static::lazy_static;
+use nix_compat::store_path;
 use rstest::rstest;
+use tvix_castore::fixtures::DUMMY_DIGEST;
 use tvix_castore::proto as castorepb;
 use tvix_castore::{DirectoryError, ValidateNodeError};
 
-#[rstest]
-#[case::no_node(None, Err(ValidatePathInfoError::NoNodePresent))]
-#[case::no_node_2(Some(castorepb::Node { node: None}), Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::NoNodeSet)))]
+lazy_static! {
+    /// A valid PathInfo message
+    /// The references in `narinfo.reference_names` aligns with what's in
+    /// `references`.
+    static ref PROTO_PATH_INFO : proto::PathInfo = proto::PathInfo {
+        node: Some(castorepb::Node {
+            node: Some(castorepb::node::Node::Directory(castorepb::DirectoryNode {
+                name: DUMMY_PATH_STR.into(),
+                digest: DUMMY_DIGEST.clone().into(),
+                size: 0,
+            })),
+        }),
+        references: vec![DUMMY_PATH_DIGEST.as_slice().into()],
+        narinfo: Some(proto::NarInfo {
+            nar_size: 0,
+            nar_sha256: DUMMY_DIGEST.clone().into(),
+            signatures: vec![],
+            reference_names: vec![DUMMY_PATH_STR.to_string()],
+            deriver: None,
+            ca: Some(proto::nar_info::Ca { r#type: proto::nar_info::ca::Hash::NarSha256.into(), digest:  DUMMY_DIGEST.clone().into() })
+        }),
+    };
+}
+
+#[test]
+fn convert_valid() {
+    let path_info = PROTO_PATH_INFO.clone();
+    PathInfo::try_from(path_info).expect("must succeed");
+}
+
+/// Create a PathInfo with a correct deriver field and ensure it succeeds.
+#[test]
+fn convert_valid_deriver() {
+    let mut path_info = PROTO_PATH_INFO.clone();
+
+    // add a valid deriver
+    let narinfo = path_info.narinfo.as_mut().unwrap();
+    narinfo.deriver = Some(crate::proto::StorePath {
+        name: DUMMY_PATH.name().to_string(),
+        digest: Bytes::from(DUMMY_PATH_DIGEST.as_slice()),
+    });
 
-fn validate_pathinfo(
+    let path_info = PathInfo::try_from(path_info).expect("must succeed");
+    assert_eq!(DUMMY_PATH.clone(), path_info.deriver.unwrap())
+}
+
+#[rstest]
+#[case::no_node(None, ValidatePathInfoError::NoNodePresent)]
+#[case::no_node_2(Some(castorepb::Node { node: None}), ValidatePathInfoError::InvalidRootNode(DirectoryError::NoNodeSet))]
+fn convert_pathinfo_wrong_nodes(
     #[case] node: Option<castorepb::Node>,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
+    #[case] exp_err: ValidatePathInfoError,
 ) {
     // construct the PathInfo object
-    let p = PathInfo {
-        node,
-        ..Default::default()
-    };
+    let mut path_info = PROTO_PATH_INFO.clone();
+    path_info.node = node;
 
-    assert_eq!(exp_result, p.validate());
+    assert_eq!(
+        exp_err,
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
+/// Constructs a [proto::PathInfo] with root nodes that have wrong data in
+/// various places, causing the conversion to [PathInfo] to fail.
 #[rstest]
-#[case::ok(castorepb::DirectoryNode {
-        name: DUMMY_PATH.into(),
-        digest: DUMMY_DIGEST.clone().into(),
-        size: 0,
-}, Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap()))]
-#[case::invalid_digest_length(castorepb::DirectoryNode {
-        name: DUMMY_PATH.into(),
+#[case::directory_invalid_digest_length(
+    castorepb::node::Node::Directory(castorepb::DirectoryNode {
+        name: DUMMY_PATH_STR.into(),
         digest: Bytes::new(),
         size: 0,
-}, Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH.into(), ValidateNodeError::InvalidDigestLen(0)))))]
-#[case::invalid_node_name_no_storepath(castorepb::DirectoryNode {
+    }),
+    ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH_STR.into(), ValidateNodeError::InvalidDigestLen(0)))
+)]
+#[case::directory_invalid_node_name_no_storepath(
+    castorepb::node::Node::Directory(castorepb::DirectoryNode {
         name: "invalid".into(),
         digest: DUMMY_DIGEST.clone().into(),
         size: 0,
-}, Err(ValidatePathInfoError::InvalidNodeName(
-        "invalid".into(),
-        store_path::Error::InvalidLength
-)))]
-fn validate_directory(
-    #[case] directory_node: castorepb::DirectoryNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Directory(directory_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
-
-#[rstest]
-#[case::ok(
-    castorepb::FileNode {
-        name: DUMMY_PATH.into(),
-        digest: DUMMY_DIGEST.clone().into(),
-        size: 0,
-        executable: false,
-    },
-    Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap())
+    }),
+    ValidatePathInfoError::InvalidNodeName("invalid".into(), store_path::Error::InvalidLength)
 )]
-#[case::invalid_digest_len(
-    castorepb::FileNode {
-        name: DUMMY_PATH.into(),
+#[case::file_invalid_digest_len(
+    castorepb::node::Node::File(castorepb::FileNode {
+        name: DUMMY_PATH_STR.into(),
         digest: Bytes::new(),
         ..Default::default()
-    },
-    Err(ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH.into(), ValidateNodeError::InvalidDigestLen(0))))
+    }),
+    ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH_STR.into(), ValidateNodeError::InvalidDigestLen(0)))
 )]
-#[case::invalid_node_name(
-    castorepb::FileNode {
+#[case::file_invalid_node_name(
+    castorepb::node::Node::File(castorepb::FileNode {
         name: "invalid".into(),
         digest: DUMMY_DIGEST.clone().into(),
         ..Default::default()
-    },
-    Err(ValidatePathInfoError::InvalidNodeName(
+    }),
+    ValidatePathInfoError::InvalidNodeName(
         "invalid".into(),
         store_path::Error::InvalidLength
-    ))
-)]
-fn validate_file(
-    #[case] file_node: castorepb::FileNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::File(file_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
-
-#[rstest]
-#[case::ok(
-    castorepb::SymlinkNode {
-        name: DUMMY_PATH.into(),
-        target: "foo".into(),
-    },
-    Ok(StorePath::from_bytes(DUMMY_PATH.as_bytes()).unwrap())
+    )
 )]
-#[case::invalid_node_name(
-    castorepb::SymlinkNode {
+#[case::symlink_invalid_node_name(
+    castorepb::node::Node::Symlink(castorepb::SymlinkNode {
         name: "invalid".into(),
         target: "foo".into(),
-    },
-    Err(ValidatePathInfoError::InvalidNodeName(
+    }),
+    ValidatePathInfoError::InvalidNodeName(
         "invalid".into(),
         store_path::Error::InvalidLength
-    ))
+    )
 )]
-fn validate_symlink(
-    #[case] symlink_node: castorepb::SymlinkNode,
-    #[case] exp_result: Result<StorePath<String>, ValidatePathInfoError>,
-) {
-    // construct the PathInfo object
-    let p = PathInfo {
-        node: Some(castorepb::Node {
-            node: Some(castorepb::node::Node::Symlink(symlink_node)),
-        }),
-        ..Default::default()
-    };
-    assert_eq!(exp_result, p.validate());
-}
+fn convert_fail_node(#[case] node: castorepb::node::Node, #[case] exp_err: ValidatePathInfoError) {
+    // construct the proto::PathInfo object
+    let mut p = PROTO_PATH_INFO.clone();
+    p.node = Some(castorepb::Node { node: Some(node) });
 
-/// Ensure parsing a correct PathInfo without narinfo populated succeeds.
-#[test]
-fn validate_references_without_narinfo_ok() {
-    assert!(PATH_INFO_WITHOUT_NARINFO.validate().is_ok());
+    assert_eq!(exp_err, PathInfo::try_from(p).expect_err("must fail"));
 }
 
-/// Ensure parsing a correct PathInfo with narinfo populated succeeds.
+/// Ensure a PathInfo without narinfo populated fails converting!
 #[test]
-fn validate_references_with_narinfo_ok() {
-    assert!(PATH_INFO_WITH_NARINFO.validate().is_ok());
+fn convert_without_narinfo_fail() {
+    let mut path_info = PROTO_PATH_INFO.clone();
+    path_info.narinfo = None;
+
+    assert_eq!(
+        ValidatePathInfoError::NarInfoFieldMissing,
+        PathInfo::try_from(path_info).expect_err("must fail"),
+    );
 }
 
 /// Create a PathInfo with a wrong digest length in narinfo.nar_sha256, and
-/// ensure validation fails.
+/// ensure conversion fails.
 #[test]
-fn validate_wrong_nar_sha256() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_wrong_nar_sha256() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.narinfo.as_mut().unwrap().nar_sha256 = vec![0xbe, 0xef].into();
 
-    match path_info.validate().expect_err("must_fail") {
-        ValidatePathInfoError::InvalidNarSha256DigestLen(2) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+    assert_eq!(
+        ValidatePathInfoError::InvalidNarSha256DigestLen(2),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a wrong count of narinfo.reference_names,
 /// and ensure validation fails.
 #[test]
-fn validate_inconsistent_num_refs_fail() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_inconsistent_num_refs_fail() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.narinfo.as_mut().unwrap().reference_names = vec![];
 
-    match path_info.validate().expect_err("must_fail") {
-        ValidatePathInfoError::InconsistentNumberOfReferences(1, 0) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+    assert_eq!(
+        ValidatePathInfoError::InconsistentNumberOfReferences(1, 0),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a wrong digest length in references.
 #[test]
-fn validate_invalid_reference_digest_len() {
-    let mut path_info = PATH_INFO_WITHOUT_NARINFO.clone();
+fn convert_invalid_reference_digest_len() {
+    let mut path_info = PROTO_PATH_INFO.clone();
     path_info.references.push(vec![0xff, 0xff].into());
 
-    match path_info.validate().expect_err("must fail") {
+    assert_eq!(
         ValidatePathInfoError::InvalidReferenceDigestLen(
             1, // position
             2, // unexpected digest len
-        ) => {}
-        e => panic!("unexpected error: {:?}", e),
-    };
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a narinfo.reference_name[1] that is no valid store path.
 #[test]
-fn validate_invalid_narinfo_reference_name() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_invalid_narinfo_reference_name() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // This is invalid, as the store prefix is not part of reference_names.
     path_info.narinfo.as_mut().unwrap().reference_names[0] =
         "/nix/store/00000000000000000000000000000000-dummy".to_string();
 
-    match path_info.validate().expect_err("must fail") {
-        ValidatePathInfoError::InvalidNarinfoReferenceName(0, reference_name) => {
-            assert_eq!(
-                "/nix/store/00000000000000000000000000000000-dummy",
-                reference_name
-            );
-        }
-        e => panic!("unexpected error: {:?}", e),
-    }
+    assert_eq!(
+        ValidatePathInfoError::InvalidNarinfoReferenceName(
+            0,
+            "/nix/store/00000000000000000000000000000000-dummy".to_string()
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    );
 }
 
 /// Create a PathInfo with a narinfo.reference_name[0] that doesn't match references[0].
 #[test]
-fn validate_inconsistent_narinfo_reference_name_digest() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_inconsistent_narinfo_reference_name_digest() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // mutate the first reference, they were all zeroes before
     path_info.references[0] = vec![0xff; store_path::DIGEST_SIZE].into();
 
-    match path_info.validate().expect_err("must fail") {
-        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(0, e_expected, e_actual) => {
-            assert_eq!(path_info.references[0][..], e_expected[..]);
-            assert_eq!(DUMMY_PATH_DIGEST, e_actual);
-        }
-        e => panic!("unexpected error: {:?}", e),
-    }
-}
-
-/// Create a node with an empty symlink target, and ensure it fails validation.
-#[test]
-fn validate_symlink_empty_target_invalid() {
-    castorepb::Node {
-        node: Some(castorepb::node::Node::Symlink(castorepb::SymlinkNode {
-            name: "foo".into(),
-            target: "".into(),
-        })),
-    }
-    .into_name_and_node()
-    .expect_err("must fail validation");
-}
-
-/// Create a node with a symlink target including null bytes, and ensure it
-/// fails validation.
-#[test]
-fn validate_symlink_target_null_byte_invalid() {
-    castorepb::Node {
-        node: Some(castorepb::node::Node::Symlink(castorepb::SymlinkNode {
-            name: "foo".into(),
-            target: "foo\0".into(),
-        })),
-    }
-    .into_name_and_node()
-    .expect_err("must fail validation");
-}
-
-/// Create a PathInfo with a correct deriver field and ensure it succeeds.
-#[test]
-fn validate_valid_deriver() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
-
-    // add a valid deriver
-    let narinfo = path_info.narinfo.as_mut().unwrap();
-    narinfo.deriver = Some(crate::proto::StorePath {
-        name: "foo".to_string(),
-        digest: Bytes::from(DUMMY_PATH_DIGEST.as_slice()),
-    });
-
-    path_info.validate().expect("must validate");
+    assert_eq!(
+        ValidatePathInfoError::InconsistentNarinfoReferenceNameDigest(
+            0,
+            path_info.references[0][..].try_into().unwrap(),
+            DUMMY_PATH_DIGEST
+        ),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    )
 }
 
 /// Create a PathInfo with a broken deriver field and ensure it fails.
 #[test]
-fn validate_invalid_deriver() {
-    let mut path_info = PATH_INFO_WITH_NARINFO.clone();
+fn convert_invalid_deriver() {
+    let mut path_info = PROTO_PATH_INFO.clone();
 
     // add a broken deriver (invalid digest)
     let narinfo = path_info.narinfo.as_mut().unwrap();
@@ -277,157 +229,8 @@ fn validate_invalid_deriver() {
         digest: vec![].into(),
     });
 
-    match path_info.validate().expect_err("must fail validation") {
-        ValidatePathInfoError::InvalidDeriverField(_) => {}
-        e => panic!("unexpected error: {:?}", e),
-    }
-}
-
-#[test]
-fn from_nixcompat_narinfo() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1
-URL: nar/1nhgq6wcggx0plpy4991h3ginj6hipsdslv4fd4zml1n707j26yq.nar.xz
-Compression: xz
-FileHash: sha256:1nhgq6wcggx0plpy4991h3ginj6hipsdslv4fd4zml1n707j26yq
-FileSize: 50088
-NarHash: sha256:0yzhigwjl6bws649vcs2asa4lbs8hg93hyix187gc7s7a74w5h80
-NarSize: 226488
-References: 3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8 s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1
-Deriver: ib3sh3pcz10wsmavxvkdbayhqivbghlq-hello-2.12.1.drv
-Sig: cache.nixos.org-1:8ijECciSFzWHwwGVOIVYdp2fOIOJAfmzGHPQVwpktfTQJF6kMPPDre7UtFw3o+VqenC5P8RikKOAAfN7CvPEAg=="#).expect("must parse");
-
-    assert_eq!(
-        PathInfo {
-            node: None,
-            references: vec![
-                Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("3n58xw4373jp0ljirf06d8077j15pc4j").unwrap()),
-                Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("s66mzxpvicwk07gjbjfw9izjfa797vsw").unwrap()),
-            ],
-            narinfo: Some(
-                NarInfo {
-                    nar_size: 226488,
-                    nar_sha256: Bytes::copy_from_slice(
-                        &nixbase32::decode_fixed::<32>("0yzhigwjl6bws649vcs2asa4lbs8hg93hyix187gc7s7a74w5h80".as_bytes())
-                            .unwrap()
-                    ),
-                    signatures: vec![Signature {
-                        name: "cache.nixos.org-1".to_string(),
-                        data: BASE64.decode("8ijECciSFzWHwwGVOIVYdp2fOIOJAfmzGHPQVwpktfTQJF6kMPPDre7UtFw3o+VqenC5P8RikKOAAfN7CvPEAg==".as_bytes()).unwrap().into(),
-                    }],
-                    reference_names: vec![
-                        "3n58xw4373jp0ljirf06d8077j15pc4j-glibc-2.37-8".to_string(),
-                        "s66mzxpvicwk07gjbjfw9izjfa797vsw-hello-2.12.1".to_string()
-                    ],
-                    deriver: Some(crate::proto::StorePath {
-                        digest: Bytes::copy_from_slice(&nixbase32::decode_fixed::<20>("ib3sh3pcz10wsmavxvkdbayhqivbghlq").unwrap()),
-                        name: "hello-2.12.1".to_string(),
-                     }),
-                    ca: None,
-                }
-            )
-        },
-        (&narinfo_parsed).into(),
-    );
-}
-
-#[test]
-fn from_nixcompat_narinfo_fod() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: nar/1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r.nar.xz
-Compression: xz
-FileHash: sha256:1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r
-FileSize: 1033524
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"#
-    ).expect("must parse");
-
-    assert_eq!(
-        PathInfo {
-            node: None,
-            references: vec![],
-            narinfo: Some(
-                NarInfo {
-                    nar_size: 1033416,
-                    nar_sha256: Bytes::copy_from_slice(
-                        &nixbase32::decode_fixed::<32>(
-                            "1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh"
-                        )
-                        .unwrap()
-                    ),
-                    signatures: vec![Signature {
-                        name: "cache.nixos.org-1".to_string(),
-                        data: BASE64
-                            .decode("ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==".as_bytes())
-                            .unwrap()
-                            .into(),
-                    }],
-                    reference_names: vec![],
-                    deriver: Some(crate::proto::StorePath {
-                        digest: Bytes::copy_from_slice(
-                            &nixbase32::decode_fixed::<20>("dyivpmlaq2km6c11i0s6bi6mbsx0ylqf").unwrap()
-                        ),
-                        name: "hello-2.12.1.tar.gz".to_string(),
-                    }),
-                    ca: Some(crate::proto::nar_info::Ca {
-                        r#type: crate::proto::nar_info::ca::Hash::FlatSha256.into(),
-                        digest: Bytes::copy_from_slice(
-                            &nixbase32::decode_fixed::<32>(
-                                "086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"
-                            )
-                            .unwrap()
-                        )
-                    }),
-                }
-            ),
-        },
-        (&narinfo_parsed).into()
-    );
-}
-
-/// Exercise .as_narinfo() on a PathInfo and ensure important fields are preserved..
-#[test]
-fn as_narinfo() {
-    let narinfo_parsed = nix_compat::narinfo::NarInfo::parse(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: nar/1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r.nar.xz
-Compression: xz
-FileHash: sha256:1zjrhzhaizsrlsvdkqfl073vivmxcqnzkff4s50i0cdf541ary1r
-FileSize: 1033524
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd"#
-    ).expect("must parse");
-
-    let path_info: PathInfo = (&narinfo_parsed).into();
-
-    let mut narinfo_returned = path_info
-        .to_narinfo(
-            StorePathRef::from_bytes(b"pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz")
-                .expect("invalid storepath"),
-        )
-        .expect("must be some");
-    narinfo_returned.url = "some.nar";
-
     assert_eq!(
-        r#"StorePath: /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
-URL: some.nar
-Compression: none
-NarHash: sha256:1lvqpbk2k1sb39z8jfxixf7p7v8sj4z6mmpa44nnmff3w1y6h8lh
-NarSize: 1033416
-References: 
-Deriver: dyivpmlaq2km6c11i0s6bi6mbsx0ylqf-hello-2.12.1.tar.gz.drv
-Sig: cache.nixos.org-1:ywnIG629nQZQhEr6/HLDrLT/mUEp5J1LC6NmWSlJRWL/nM7oGItJQUYWGLvYGhSQvHrhIuvMpjNmBNh/WWqCDg==
-CA: fixed:sha256:086vqwk2wl8zfs47sq2xpjc9k066ilmb8z6dn0q6ymwjzlm196cd
-"#,
-        narinfo_returned.to_string(),
-    );
+        ValidatePathInfoError::InvalidDeriverField(store_path::Error::InvalidLength),
+        PathInfo::try_from(path_info).expect_err("must fail")
+    )
 }