about summary refs log tree commit diff
path: root/tvix/store/src/proto/tests/pathinfo.rs
use crate::proto::{PathInfo, ValidatePathInfoError};
use crate::tests::fixtures::*;
use bytes::Bytes;
use nix_compat::store_path::{self, StorePath};
use std::str::FromStr;
use test_case::test_case;
use tvix_castore::proto as castorepb;

#[test_case(
    None,
    Err(ValidatePathInfoError::NoNodePresent()) ;
    "No node"
)]
#[test_case(
    Some(castorepb::Node { node: None }),
    Err(ValidatePathInfoError::NoNodePresent());
    "No node 2"
)]
fn validate_no_node(
    t_node: Option<castorepb::Node>,
    t_result: Result<StorePath, ValidatePathInfoError>,
) {
    // construct the PathInfo object
    let p = PathInfo {
        node: t_node,
        ..Default::default()
    };
    assert_eq!(t_result, p.validate());
}

#[test_case(
    castorepb::DirectoryNode {
        name: DUMMY_NAME.into(),
        digest: DUMMY_DIGEST.clone().into(),
        size: 0,
    },
    Ok(StorePath::from_str(DUMMY_NAME).expect("must succeed"));
    "ok"
)]
#[test_case(
    castorepb::DirectoryNode {
        name: DUMMY_NAME.into(),
        digest: Bytes::new(),
        size: 0,
    },
    Err(ValidatePathInfoError::InvalidRootNode(castorepb::ValidateNodeError::InvalidDigestLen(0)));
    "invalid digest length"
)]
#[test_case(
    castorepb::DirectoryNode {
        name: "invalid".into(),
        digest: DUMMY_DIGEST.clone().into(),
        size: 0,
    },
    Err(ValidatePathInfoError::InvalidNodeName(
        "invalid".into(),
        store_path::Error::InvalidLength()
    ));
    "invalid node name"
)]
fn validate_directory(
    t_directory_node: castorepb::DirectoryNode,
    t_result: Result<StorePath, ValidatePathInfoError>,
) {
    // construct the PathInfo object
    let p = PathInfo {
        node: Some(castorepb::Node {
            node: Some(castorepb::node::Node::Directory(t_directory_node)),
        }),
        ..Default::default()
    };
    assert_eq!(t_result, p.validate());
}

#[test_case(
    castorepb::FileNode {
        name: DUMMY_NAME.into(),
        digest: DUMMY_DIGEST.clone().into(),
        size: 0,
        executable: false,
    },
    Ok(StorePath::from_str(DUMMY_NAME).expect("must succeed"));
    "ok"
)]
#[test_case(
    castorepb::FileNode {
        name: DUMMY_NAME.into(),
        digest: Bytes::new(),
        ..Default::default()
    },
    Err(ValidatePathInfoError::InvalidRootNode(castorepb::ValidateNodeError::InvalidDigestLen(0)));
    "invalid digest length"
)]
#[test_case(
    castorepb::FileNode {
        name: "invalid".into(),
        digest: DUMMY_DIGEST.clone().into(),
        ..Default::default()
    },
    Err(ValidatePathInfoError::InvalidNodeName(
        "invalid".into(),
        store_path::Error::InvalidLength()
    ));
    "invalid node name"
)]
fn validate_file(
    t_file_node: castorepb::FileNode,
    t_result: Result<StorePath, ValidatePathInfoError>,
) {
    // construct the PathInfo object
    let p = PathInfo {
        node: Some(castorepb::Node {
            node: Some(castorepb::node::Node::File(t_file_node)),
        }),
        ..Default::default()
    };
    assert_eq!(t_result, p.validate());
}

#[test_case(
    castorepb::SymlinkNode {
        name: DUMMY_NAME.into(),
        target: "foo".into(),
    },
    Ok(StorePath::from_str(DUMMY_NAME).expect("must succeed"));
    "ok"
)]
#[test_case(
    castorepb::SymlinkNode {
        name: "invalid".into(),
        target: "foo".into(),
    },
    Err(ValidatePathInfoError::InvalidNodeName(
        "invalid".into(),
        store_path::Error::InvalidLength()
    ));
    "invalid node name"
)]
fn validate_symlink(
    t_symlink_node: castorepb::SymlinkNode,
    t_result: Result<StorePath, ValidatePathInfoError>,
) {
    // construct the PathInfo object
    let p = PathInfo {
        node: Some(castorepb::Node {
            node: Some(castorepb::node::Node::Symlink(t_symlink_node)),
        }),
        ..Default::default()
    };
    assert_eq!(t_result, p.validate());
}

/// Ensure parsing a correct PathInfo without narinfo populated succeeds.
#[test]
fn validate_references_without_narinfo_ok() {
    assert!(PATH_INFO_WITHOUT_NARINFO.validate().is_ok());
}

/// Ensure parsing a correct PathInfo with narinfo populated succeeds.
#[test]
fn validate_references_with_narinfo_ok() {
    assert!(PATH_INFO_WITH_NARINFO.validate().is_ok());
}

/// Create a PathInfo with a wrong digest length in narinfo.nar_sha256, and
/// ensure validation fails.
#[test]
fn validate_wrong_nar_sha256() {
    let mut path_info = PATH_INFO_WITH_NARINFO.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),
    };
}

/// 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();
    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),
    };
}

/// 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();
    path_info.references.push(vec![0xff, 0xff].into());

    match path_info.validate().expect_err("must fail") {
        ValidatePathInfoError::InvalidReferenceDigestLen(
            1, // position
            2, // unexpected digest len
        ) => {}
        e => panic!("unexpected error: {:?}", e),
    };
}

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

    // 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),
    }
}

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

    // 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_OUTPUT_HASH[..], 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() {
    let node = castorepb::node::Node::Symlink(castorepb::SymlinkNode {
        name: "foo".into(),
        target: "".into(),
    });

    node.validate().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() {
    let node = castorepb::node::Node::Symlink(castorepb::SymlinkNode {
        name: "foo".into(),
        target: "foo\0".into(),
    });

    node.validate().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_WITHOUT_NARINFO.clone();

    // add a valid deriver
    path_info.deriver = Some(crate::proto::StorePath {
        name: "foo".to_string(),
        digest: DUMMY_OUTPUT_HASH.clone(),
    });

    path_info.validate().expect("must validate");
}

/// Create a PathInfo with a broken deriver field and ensure it fails.
#[test]
fn validate_invalid_deriver() {
    let mut path_info = PATH_INFO_WITHOUT_NARINFO.clone();

    // add a broken deriver (invalid digest)
    path_info.deriver = Some(crate::proto::StorePath {
        name: "foo".to_string(),
        digest: vec![].into(),
    });

    match path_info.validate().expect_err("must fail validation") {
        ValidatePathInfoError::InvalidDeriverField(_) => {}
        e => panic!("unexpected error: {:?}", e),
    }
}