acyclic-fs 0.1.5

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

#[test]
fn windows_names_validate_utf16_without_lossy_normalization()
-> Result<(), Box<dyn std::error::Error>> {
    let emoji: Vec<u8> = "😀.rs".encode_utf16().flat_map(u16::to_le_bytes).collect();
    let name = LogicalName::new(NameEncoding::WindowsUtf16Le, emoji.clone(), 255)?;
    assert_eq!(name.as_bytes(), emoji);

    for invalid in [
        vec![0x00, 0xd8],
        vec![b'/', 0],
        vec![b'\\', 0],
        vec![b'.', 0],
        vec![b'.', 0, b'.', 0],
    ] {
        assert!(matches!(
            LogicalName::new(NameEncoding::WindowsUtf16Le, invalid, 255),
            Err(TreePageError::InvalidName)
        ));
    }
    Ok(())
}

#[test]
fn names_kinds_and_profiles_are_total() -> Result<(), Box<dyn std::error::Error>> {
    for (encoding, tag) in [
        (NameEncoding::Utf8, 1),
        (NameEncoding::PosixBytes, 2),
        (NameEncoding::WindowsUtf16Le, 3),
    ] {
        assert_eq!(encoding.tag(), tag);
        assert_eq!(NameEncoding::from_tag(tag)?, encoding);
    }
    assert_eq!(
        NameEncoding::from_tag(0),
        Err(TreePageError::UnknownNameEncoding(0))
    );
    assert_eq!(
        LogicalName::new(NameEncoding::Utf8, Vec::new(), 8),
        Err(TreePageError::EmptyName)
    );
    assert_eq!(
        LogicalName::new(NameEncoding::Utf8, b"abc".to_vec(), 2),
        Err(TreePageError::NameTooLarge)
    );
    for (encoding, bytes) in [
        (NameEncoding::Utf8, vec![0xff]),
        (NameEncoding::Utf8, b".".to_vec()),
        (NameEncoding::Utf8, b"..".to_vec()),
        (NameEncoding::Utf8, b"a/b".to_vec()),
        (NameEncoding::Utf8, b"a\0b".to_vec()),
        (NameEncoding::PosixBytes, b".".to_vec()),
        (NameEncoding::PosixBytes, b"..".to_vec()),
        (NameEncoding::PosixBytes, b"a/b".to_vec()),
        (NameEncoding::PosixBytes, b"a\0b".to_vec()),
        (NameEncoding::WindowsUtf16Le, vec![b'a']),
    ] {
        assert_eq!(
            LogicalName::new(encoding, bytes, 8),
            Err(TreePageError::InvalidName)
        );
    }

    let kinds = [
        FileKind::Regular,
        FileKind::Directory,
        FileKind::SymbolicLink,
        FileKind::Fifo,
        FileKind::Socket,
        FileKind::CharacterDevice,
        FileKind::BlockDevice,
        FileKind::ReparsePoint,
        FileKind::MountBoundary,
    ];
    for (index, kind) in kinds.into_iter().enumerate() {
        let tag = u8::try_from(index + 1)?;
        assert_eq!(kind.tag(), tag);
        assert_eq!(FileKind::from_tag(tag)?, kind);
    }
    assert_eq!(
        FileKind::from_tag(0),
        Err(TreePageError::UnknownFileKind(0))
    );
    for kind in [
        FileKind::Regular,
        FileKind::Directory,
        FileKind::SymbolicLink,
        FileKind::MountBoundary,
    ] {
        for profile in [
            FilesystemProfile::Portable,
            FilesystemProfile::Posix,
            FilesystemProfile::Windows,
            FilesystemProfile::Browser,
        ] {
            assert!(kind.is_supported_by_profile(profile));
        }
    }
    for kind in [
        FileKind::Fifo,
        FileKind::Socket,
        FileKind::CharacterDevice,
        FileKind::BlockDevice,
    ] {
        assert!(kind.is_supported_by_profile(FilesystemProfile::Posix));
        assert!(!kind.is_supported_by_profile(FilesystemProfile::Portable));
    }
    assert!(FileKind::ReparsePoint.is_supported_by_profile(FilesystemProfile::Windows));
    assert!(!FileKind::ReparsePoint.is_supported_by_profile(FilesystemProfile::Posix));
    Ok(())
}

#[test]
fn tree_page_validation_rejects_every_malformed_shape() -> Result<(), Box<dyn std::error::Error>> {
    let a = LogicalName::new(NameEncoding::Utf8, b"a".to_vec(), 8)?;
    let b = LogicalName::new(NameEncoding::Utf8, b"b".to_vec(), 8)?;
    let entry = |name| TreeEntry {
        name,
        file_id: FileId::from_bytes([1; 16]),
        kind: FileKind::Regular,
    };
    assert_eq!(TreePage::Leaf(Vec::new()).validate(0), Ok(()));
    assert_eq!(
        TreePage::Leaf(vec![entry(a.clone())]).validate(0),
        Err(TreePageError::TooManyItems)
    );
    assert_eq!(
        TreePage::Leaf(vec![entry(b.clone()), entry(a.clone())]).validate(2),
        Err(TreePageError::NamesNotStrictlyOrdered)
    );
    assert_eq!(
        TreePage::Internal(Vec::new()).validate(2),
        Err(TreePageError::EmptyInternalPage)
    );
    assert_eq!(
        TreePage::Internal(vec![TreeChild {
            first_name: a.clone(),
            page: digest_object(ObjectKind::Blob, [1; 32]),
        }])
        .validate(2),
        Err(TreePageError::WrongChildKind)
    );
    assert_eq!(
        TreePage::Internal(vec![
            TreeChild {
                first_name: b,
                page: digest_object(ObjectKind::TreePage, [1; 32]),
            },
            TreeChild {
                first_name: a,
                page: digest_object(ObjectKind::TreePage, [2; 32]),
            },
        ])
        .validate(2),
        Err(TreePageError::NamesNotStrictlyOrdered)
    );
    Ok(())
}

#[test]
fn extent_page_validation_rejects_every_malformed_shape() {
    let page = |kind| digest_object(kind, [1; 32]);
    let extent = |offset, length, kind| Extent {
        offset,
        length,
        kind,
    };
    let cases = [
        (
            ExtentPage::Leaf(vec![extent(0, 0, ExtentKind::Hole)]),
            ExtentPageError::ZeroLength,
        ),
        (
            ExtentPage::Leaf(vec![extent(u64::MAX, 2, ExtentKind::Hole)]),
            ExtentPageError::RangeOverflow,
        ),
        (
            ExtentPage::Leaf(vec![
                extent(0, 1, ExtentKind::Hole),
                extent(2, 1, ExtentKind::AllocatedZero),
            ]),
            ExtentPageError::NonContiguous,
        ),
        (
            ExtentPage::Leaf(vec![extent(
                0,
                1,
                ExtentKind::Content {
                    object: page(ObjectKind::Metadata),
                    object_offset: 0,
                },
            )]),
            ExtentPageError::WrongContentKind,
        ),
        (
            ExtentPage::Leaf(vec![extent(
                0,
                2,
                ExtentKind::Content {
                    object: page(ObjectKind::Blob),
                    object_offset: u64::MAX,
                },
            )]),
            ExtentPageError::RangeOverflow,
        ),
        (
            ExtentPage::Internal(Vec::new()),
            ExtentPageError::EmptyInternalPage,
        ),
        (
            ExtentPage::Internal(vec![ExtentChild {
                first_offset: 1,
                end_offset: 1,
                page: page(ObjectKind::ExtentPage),
            }]),
            ExtentPageError::InvalidChildRange,
        ),
        (
            ExtentPage::Internal(vec![ExtentChild {
                first_offset: 0,
                end_offset: 1,
                page: page(ObjectKind::Blob),
            }]),
            ExtentPageError::WrongChildKind,
        ),
        (
            ExtentPage::Internal(vec![
                ExtentChild {
                    first_offset: 0,
                    end_offset: 1,
                    page: page(ObjectKind::ExtentPage),
                },
                ExtentChild {
                    first_offset: 2,
                    end_offset: 3,
                    page: page(ObjectKind::ExtentPage),
                },
            ]),
            ExtentPageError::NonContiguous,
        ),
    ];
    for (page, error) in cases {
        assert_eq!(page.validate(8), Err(error));
    }
    assert_eq!(
        ExtentPage::Leaf(vec![extent(0, 1, ExtentKind::Hole)]).validate(0),
        Err(ExtentPageError::TooManyItems)
    );
}