links-fs 0.21.1

LFS: a links file system driver (Linux, macOS, Windows) with an embedded doublets links database, one shared journal and no fragmentation
Documentation
//! Regions of the volume: superblock, journal and chunk tables.

use super::{format_time, hex, Node, Part, DESCRIPTORS_OFFSET, DESCRIPTOR_SIZE, JOURNAL_HEADER};
use crate::codec::{get_i64, get_u32, get_u64};
use crate::device::BlockDevice;
use crate::fs::map::{array_name, Owner};
use crate::fs::Fs;
use crate::journal::DESCRIPTOR_MAGIC;
use crate::links::format_ref;
use crate::superblock::{ARRAYS, MAGIC};

impl<D: BlockDevice> Fs<D> {
    pub(super) fn volume_children(&self) -> Vec<Node> {
        let bs = self.bs();
        self.space_map()
            .extents
            .into_iter()
            .map(|extent| {
                let offset = extent.start * bs;
                let size = extent.len * bs;
                let blocks = if extent.len == 1 {
                    format!("block {}", extent.start)
                } else {
                    format!("blocks {}..{}", extent.start, extent.start + extent.len)
                };
                match extent.owner {
                    Owner::Superblock => Node::new("superblock", "superblock", offset, size)
                        .note(blocks)
                        .part(Part::Superblock),
                    Owner::Journal => Node::new("journal", "journal", offset, size)
                        .note(blocks)
                        .part(Part::Journal),
                    Owner::Table { array } => Node::new(
                        format!("{} chunk table", array_name(array)),
                        "table",
                        offset,
                        size,
                    )
                    .note(blocks)
                    .part(Part::Table(array)),
                    Owner::Chunk { array, index } => Node::new(
                        format!("{} chunk {index}", array_name(array)),
                        "chunk",
                        offset,
                        size,
                    )
                    .note(blocks)
                    .part(Part::Chunk { array, index }),
                    Owner::File { ino } => {
                        let path = self.path_of(ino).unwrap_or_else(|_| format!("#{ino}"));
                        Node::new(path, "file", offset, size)
                            .note(format!("inode {ino}, {blocks}"))
                            .part(Part::File { ino })
                    }
                    Owner::Free => Node::new("free", "free", offset, size).note(blocks),
                    Owner::Pending => Node::new("pending", "pending", offset, size)
                        .note(format!("{blocks}, freed since the last commit")),
                }
            })
            .collect()
    }

    pub(super) fn superblock_children(&self, b: &[u8]) -> Vec<Node> {
        let size = b.len() as u64;
        let u64_at = |offset: usize| get_u64(b, offset).to_string();
        vec![
            Node::new("magic", "field", 0, 8)
                .value(ascii(&b[..8]))
                .note(if &b[..8] == MAGIC {
                    "LFS image"
                } else {
                    "bad magic"
                }),
            Node::new("version", "field", 8, 4).value(get_u32(b, 8).to_string()),
            Node::new("block_size", "field", 12, 4).value(get_u32(b, 12).to_string()),
            Node::new("total_blocks", "field", 16, 8).value(u64_at(16)),
            Node::new("journal_start", "field", 24, 8).value(u64_at(24)),
            Node::new("journal_blocks", "field", 32, 8).value(u64_at(32)),
            Node::new("layout", "field", 40, 1)
                .value(b[40].to_string())
                .note(self.sb.layout.name()),
            Node::new("name_encoding", "field", 41, 1)
                .value(b[41].to_string())
                .note(self.sb.name_encoding.name()),
            Node::new("seq", "field", 48, 8)
                .value(u64_at(48))
                .note("sequence number of the last commit"),
            Node::new("links header", "header", 56, 48).part(Part::LinksHeader),
            Node::new("root", "link", 104, 8)
                .value(u64_at(104))
                .note("link of the root directory inode"),
            Node::new("config", "link", 112, 8)
                .value(format_ref(get_u64(b, 112)))
                .note("link of the persisted runtime configuration"),
            Node::new("volume_id", "field", 120, 16).value(hex(&b[120..136])),
            Node::new("label", "field", 136, 32).value(format!("{:?}", self.sb.label)),
            Node::new("created", "field", 168, 8).value(format_time(get_i64(b, 168))),
            Node::new("index_mode", "field", 176, 1)
                .value(b[176].to_string())
                .note(match b[176] {
                    0 => "off",
                    1 => "tree",
                    2 => "bitstring",
                    3 => "adaptive",
                    _ => "unknown",
                }),
            Node::new("index_threshold", "field", 184, 8)
                .value(f64::from_bits(get_u64(b, 184)).to_string())
                .note("bit string density threshold (f64)"),
            Node::new("index_hysteresis", "field", 192, 8)
                .value(f64::from_bits(get_u64(b, 192)).to_string())
                .note("fraction of the threshold below which a bit string becomes a tree (f64)"),
            Node::new(
                "array descriptors",
                "header",
                DESCRIPTORS_OFFSET,
                ARRAYS as u64 * DESCRIPTOR_SIZE,
            )
            .note("chunk table extent and chunk count of every array")
            .part(Part::Descriptors),
            Node::new("crc32", "field", size - 4, 4)
                .value(format!("{:#010x}", get_u32(b, size as usize - 4)))
                .note("CRC-32 of the block before it"),
        ]
    }

    pub(super) fn journal_children(&self, node: &Node, bytes: &[u8]) -> Vec<Node> {
        let bs = self.bs();
        if bytes.len() < 8 || &bytes[..8] != DESCRIPTOR_MAGIC {
            return vec![Node::new("empty", "unused", node.offset, node.size)
                .note("no transaction has been written here")];
        }
        let seq = get_u64(bytes, 8);
        let count = get_u64(bytes, 16);
        let per = (bs - JOURNAL_HEADER) / 8;
        let descriptors = count.div_ceil(per).max(1);
        let blocks = node.size / bs;
        if descriptors + count + 1 > blocks {
            return vec![
                Node::new("torn transaction", "unused", node.offset, node.size).note(format!(
                    "seq {seq} claims {count} blocks, more than the region holds"
                )),
            ];
        }
        let state = if seq <= self.sb.seq {
            "already checkpointed"
        } else {
            "not checkpointed: replayed at the next mount if its CRC is valid"
        };
        let mut out = Vec::new();
        for index in 0..descriptors {
            out.push(
                Node::new(
                    format!("descriptor {index}"),
                    "journal",
                    node.offset + index * bs,
                    bs,
                )
                .note(format!("transaction {seq}, {state}"))
                .part(Part::JournalDescriptor { first: index * per }),
            );
        }
        let targets: Vec<u64> = (0..count)
            .map(|slot| {
                let block = (slot / per) as usize;
                let at = block * bs as usize + JOURNAL_HEADER as usize + (slot % per) as usize * 8;
                get_u64(bytes, at)
            })
            .collect();
        for (index, target) in targets.iter().enumerate() {
            out.push(
                Node::new(
                    format!("copy of block {target}"),
                    "data",
                    node.offset + (descriptors + index as u64) * bs,
                    bs,
                )
                .note(format!(
                    "new contents of block {target} in transaction {seq}"
                )),
            );
        }
        out.push(
            Node::new(
                "commit",
                "journal",
                node.offset + (descriptors + count) * bs,
                bs,
            )
            .note(format!("transaction {seq} is durable once this block is"))
            .part(Part::JournalCommit),
        );
        out
    }

    pub(super) fn table_children(&self, node: &Node, bytes: &[u8], array: usize) -> Vec<Node> {
        let chunks = self.sb.arrays[array].chunks.min(node.size / 8);
        (0..chunks)
            .map(|index| {
                let block = get_u64(bytes, index as usize * 8);
                Node::new(
                    format!("chunk {index}"),
                    "field",
                    node.offset + index * 8,
                    8,
                )
                .value(block.to_string())
                .note(format!("block of {} chunk {index}", array_name(array)))
            })
            .collect()
    }
}

pub(super) fn ascii(bytes: &[u8]) -> String {
    bytes
        .iter()
        .map(|&byte| {
            if byte.is_ascii_graphic() {
                char::from(byte).to_string()
            } else {
                format!("\\x{byte:02x}")
            }
        })
        .collect()
}

pub(super) fn cells(
    at: u64,
    bytes: &[u8],
    fields: &[(&str, &str)],
    value: impl Fn(u64) -> String,
) -> Vec<Node> {
    fields
        .iter()
        .enumerate()
        .map(|(index, (name, note))| {
            Node::new(*name, "field", at + index as u64 * 8, 8)
                .value(value(get_u64(bytes, index * 8)))
                .note(*note)
        })
        .collect()
}

pub(super) fn journal_descriptor(node: &Node, bytes: &[u8], first: u64) -> Vec<Node> {
    let count = get_u64(bytes, 16);
    let per = (node.size - JOURNAL_HEADER) / 8;
    let mut out = vec![
        Node::new("magic", "field", node.offset, 8).value(ascii(&bytes[..8])),
        Node::new("seq", "field", node.offset + 8, 8).value(get_u64(bytes, 8).to_string()),
        Node::new("count", "field", node.offset + 16, 8)
            .value(count.to_string())
            .note("blocks in the transaction"),
    ];
    for slot in 0..per.min(count.saturating_sub(first)) {
        out.push(
            Node::new(
                format!("target {}", first + slot),
                "field",
                node.offset + JOURNAL_HEADER + slot * 8,
                8,
            )
            .value(get_u64(bytes, (JOURNAL_HEADER + slot * 8) as usize).to_string())
            .note("block the data block is written to"),
        );
    }
    out
}