acyclic-fs 0.1.1

Rust SDK for versioned workspaces and embedded native mounts
Documentation
use super::*;
use crate::foundation::Digest;

#[test]
fn classes_and_raw_names_round_trip() -> Result<(), Box<dyn std::error::Error>> {
    let page = AttributePage::Leaf(vec![AttributeEntry {
        name: AttributeName::new(AttributeClass::PosixXattr, b"user.test".to_vec(), 255)?,
        value_bytes: 4,
        value: ObjectId {
            kind: ObjectKind::Blob,
            digest: Digest::from_bytes([9; 32]),
        },
    }]);
    let bytes = encode_attribute_page(&page, 8)?;
    assert_eq!(
        decode_attribute_page(&bytes, DecodeLimits::default())?,
        page
    );
    Ok(())
}

#[test]
fn every_attribute_class_and_name_boundary_is_canonical() -> Result<(), Box<dyn std::error::Error>>
{
    for (class, tag) in [
        (AttributeClass::PosixXattr, 1),
        (AttributeClass::WindowsStream, 2),
        (AttributeClass::MacResourceFork, 3),
    ] {
        assert_eq!(class.tag(), tag);
        assert_eq!(AttributeClass::from_tag(tag)?, class);
    }
    assert_eq!(
        AttributeClass::from_tag(0),
        Err(AttributeError::UnknownClass(0))
    );
    assert_eq!(
        AttributeName::new(AttributeClass::PosixXattr, Vec::new(), 8),
        Err(AttributeError::InvalidName)
    );
    assert_eq!(
        AttributeName::new(AttributeClass::PosixXattr, b"a\0b".to_vec(), 8),
        Err(AttributeError::InvalidName)
    );
    assert_eq!(
        AttributeName::new(AttributeClass::PosixXattr, b"abc".to_vec(), 2),
        Err(AttributeError::InvalidName)
    );
    Ok(())
}

#[test]
fn every_attribute_page_shape_and_identity_is_canonical() -> Result<(), Box<dyn std::error::Error>>
{
    let first = AttributeName::new(AttributeClass::PosixXattr, b"a".to_vec(), 8)?;
    let second = AttributeName::new(AttributeClass::WindowsStream, b"b".to_vec(), 8)?;
    let blob = ObjectId {
        kind: ObjectKind::Blob,
        digest: Digest::from_bytes([1; 32]),
    };
    let attribute_page = ObjectId {
        kind: ObjectKind::AttributePage,
        digest: Digest::from_bytes([2; 32]),
    };
    let leaf = AttributePage::Leaf(vec![
        AttributeEntry {
            name: first.clone(),
            value_bytes: 3,
            value: blob,
        },
        AttributeEntry {
            name: second.clone(),
            value_bytes: 0,
            value: ObjectId {
                kind: ObjectKind::Blob,
                digest: Digest::from_bytes([3; 32]),
            },
        },
    ]);
    let internal = AttributePage::Internal(vec![
        AttributeChild {
            first_name: first.clone(),
            page: attribute_page,
        },
        AttributeChild {
            first_name: second.clone(),
            page: ObjectId {
                kind: ObjectKind::AttributePage,
                digest: Digest::from_bytes([4; 32]),
            },
        },
    ]);
    let limits = DecodeLimits {
        maximum_page_items: 2,
        maximum_name_bytes: 8,
        ..DecodeLimits::default()
    };
    for (page, kind) in [
        (leaf.clone(), DecodedPageKind::Leaf),
        (internal.clone(), DecodedPageKind::Internal),
    ] {
        let encoded = encode_attribute_page(&page, 2)?;
        let shape = attribute_page_decode_shape(&encoded, limits)?;
        assert_eq!(shape.kind, kind);
        assert_eq!(shape.items, 2);
        assert_eq!(shape.nested_bytes, 2);
        assert_eq!(decode_attribute_page(&encoded, limits)?, page);
        let object_id = attribute_page_id(&page, 2)?;
        assert_eq!(object_id.kind, ObjectKind::AttributePage);
        assert_eq!(
            object_id.digest,
            object_digest(ObjectKind::AttributePage, &encoded)
        );
    }
    assert_eq!(
        attribute_leaf_page_encoded_length(
            match &leaf {
                AttributePage::Leaf(v) => v,
                AttributePage::Internal(_) => unreachable!(),
            },
            2
        )?,
        encode_attribute_page(&leaf, 2)?.len()
    );
    assert_eq!(
        attribute_internal_page_encoded_length(
            match &internal {
                AttributePage::Internal(v) => v.iter().map(|child| (&child.first_name, child.page)),
                AttributePage::Leaf(_) => unreachable!(),
            },
            2
        )?,
        encode_attribute_page(&internal, 2)?.len()
    );
    Ok(())
}

#[test]
fn malformed_attribute_pages_fail_at_every_public_boundary()
-> Result<(), Box<dyn std::error::Error>> {
    let name = AttributeName::new(AttributeClass::PosixXattr, b"a".to_vec(), 8)?;
    let blob = ObjectId {
        kind: ObjectKind::Blob,
        digest: Digest::from_bytes([1; 32]),
    };
    let wrong = ObjectId {
        kind: ObjectKind::Metadata,
        digest: Digest::from_bytes([2; 32]),
    };
    let entry = AttributeEntry {
        name: name.clone(),
        value_bytes: 1,
        value: blob,
    };
    let duplicate = AttributePage::Leaf(vec![entry.clone(), entry.clone()]);
    let wrong_leaf = AttributePage::Leaf(vec![AttributeEntry {
        value: wrong,
        ..entry.clone()
    }]);
    let empty_internal = AttributePage::Internal(Vec::new());
    let wrong_internal = AttributePage::Internal(vec![AttributeChild {
        first_name: name.clone(),
        page: wrong,
    }]);
    for page in [&duplicate, &wrong_leaf, &empty_internal, &wrong_internal] {
        assert!(encode_attribute_page(page, 8).is_err());
        assert_eq!(page.validate(8), Err(AttributeError::InvalidPage));
    }
    assert_eq!(
        AttributePage::Leaf(vec![entry.clone()]).validate(0),
        Err(AttributeError::TooManyItems)
    );
    assert_eq!(
        AttributePage::Internal(vec![AttributeChild {
            first_name: name,
            page: ObjectId {
                kind: ObjectKind::AttributePage,
                digest: Digest::from_bytes([3; 32]),
            },
        }])
        .validate(0),
        Err(AttributeError::TooManyItems)
    );
    assert!(encode_attribute_page(&AttributePage::Leaf(vec![entry.clone()]), 0).is_err());
    assert!(attribute_page_id(&duplicate, 8).is_err());

    let valid = encode_attribute_page(&AttributePage::Leaf(vec![entry]), 1)?;
    let mut unknown_page = valid.clone();
    unknown_page[DOMAIN.len() + 2] = 9;
    assert!(matches!(
        attribute_page_decode_shape(&unknown_page, DecodeLimits::default()),
        Err(CanonicalDecodeError::UnknownTag {
            field: "attribute_page",
            tag: 9
        })
    ));
    assert!(matches!(
        decode_attribute_page(&unknown_page, DecodeLimits::default()),
        Err(CanonicalDecodeError::UnknownTag {
            field: "attribute_page",
            tag: 9
        })
    ));

    let mut unknown_class = valid.clone();
    unknown_class[DOMAIN.len() + 2 + 1 + 4] = 9;
    assert!(matches!(
        decode_attribute_page(&unknown_class, DecodeLimits::default()),
        Err(CanonicalDecodeError::Invariant(_))
    ));

    let tight_items = DecodeLimits {
        maximum_page_items: 0,
        ..DecodeLimits::default()
    };
    assert!(matches!(
        attribute_page_decode_shape(&valid, tight_items),
        Err(CanonicalDecodeError::FieldTooLarge {
            observed: 1,
            maximum: 0
        })
    ));
    assert!(matches!(
        decode_attribute_page(&valid, tight_items),
        Err(CanonicalDecodeError::FieldTooLarge {
            observed: 1,
            maximum: 0
        })
    ));
    Ok(())
}