about summary refs log tree commit diff
path: root/tvix/store/src/proto/tests/pathinfo.rs
use std::sync::LazyLock;

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 nix_compat::store_path;
use rstest::rstest;
use tvix_castore::fixtures::DUMMY_DIGEST;
use tvix_castore::proto as castorepb;
use tvix_castore::{DirectoryError, ValidateNodeError};

/// A valid PathInfo message
/// The references in `narinfo.reference_names` aligns with what's in
/// `references`.
static PROTO_PATH_INFO: LazyLock<proto::PathInfo> = LazyLock::new(|| 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()),
    });

    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_err: ValidatePathInfoError,
) {
    // construct the PathInfo object
    let mut path_info = PROTO_PATH_INFO.clone();
    path_info.node = node;

    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::directory_invalid_digest_length(
    castorepb::node::Node::Directory(castorepb::DirectoryNode {
        name: DUMMY_PATH_STR.into(),
        digest: Bytes::new(),
        size: 0,
    }),
    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,
    }),
    ValidatePathInfoError::InvalidNodeName("invalid".into(), store_path::Error::InvalidLength)
)]
#[case::file_invalid_digest_len(
    castorepb::node::Node::File(castorepb::FileNode {
        name: DUMMY_PATH_STR.into(),
        digest: Bytes::new(),
        ..Default::default()
    }),
    ValidatePathInfoError::InvalidRootNode(DirectoryError::InvalidNode(DUMMY_PATH_STR.into(), ValidateNodeError::InvalidDigestLen(0)))
)]
#[case::file_invalid_node_name(
    castorepb::node::Node::File(castorepb::FileNode {
        name: "invalid".into(),
        digest: DUMMY_DIGEST.clone().into(),
        ..Default::default()
    }),
    ValidatePathInfoError::InvalidNodeName(
        "invalid".into(),
        store_path::Error::InvalidLength
    )
)]
#[case::symlink_invalid_node_name(
    castorepb::node::Node::Symlink(castorepb::SymlinkNode {
        name: "invalid".into(),
        target: "foo".into(),
    }),
    ValidatePathInfoError::InvalidNodeName(
        "invalid".into(),
        store_path::Error::InvalidLength
    )
)]
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) });

    assert_eq!(exp_err, PathInfo::try_from(p).expect_err("must fail"));
}

/// Ensure a PathInfo without narinfo populated fails converting!
#[test]
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 conversion fails.
#[test]
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();

    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 convert_inconsistent_num_refs_fail() {
    let mut path_info = PROTO_PATH_INFO.clone();
    path_info.narinfo.as_mut().unwrap().reference_names = vec![];

    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 convert_invalid_reference_digest_len() {
    let mut path_info = PROTO_PATH_INFO.clone();
    path_info.references.push(vec![0xff, 0xff].into());

    assert_eq!(
        ValidatePathInfoError::InvalidReferenceDigestLen(
            1, // position
            2, // unexpected digest len
        ),
        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 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();

    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 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();

    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 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();
    narinfo.deriver = Some(crate::proto::StorePath {
        name: "foo".to_string(),
        digest: vec![].into(),
    });

    assert_eq!(
        ValidatePathInfoError::InvalidDeriverField(store_path::Error::InvalidLength),
        PathInfo::try_from(path_info).expect_err("must fail")
    )
}