gpt-parser 0.0.10

GUID Partition Table (GPT) read-only parser. Endian aware and usable against no_std+alloc.
Documentation
// Copyright 2022 Alberto Ruiz <aruiz@redhat.com>
//
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

#![cfg_attr(feature = "no_std", no_std)]

use crc::Crc;

use core::char::{decode_utf16, DecodeUtf16Error};

#[cfg(feature = "no_std")]
extern crate alloc;

#[cfg(feature = "no_std")]
use alloc::string::String;

#[cfg(feature = "no_std")]
use core::{mem, ops};

#[cfg(not(feature = "no_std"))]
use std::{mem, ops};

use ops::{Index, Range};

use mem::size_of;

use explicit_endian::LittleEndian;

#[repr(C, align(1))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Uuid {
    time_low: LittleEndian<u32>,
    time_mid: LittleEndian<u16>,
    time_hi_and_version: LittleEndian<u16>,
    clock_seq_hi_and_reseved: u8,
    clock_seq_loq: u8,
    node: [u8; 6],
}

impl Uuid {
    pub fn nil() -> Self {
        Self {
            time_low: 0.into(),
            time_mid: 0.into(),
            time_hi_and_version: 0.into(),
            clock_seq_hi_and_reseved: 0,
            clock_seq_loq: 0,
            node: [0, 0, 0, 0, 0, 0],
        }
    }

    pub fn efi() -> Self {
        Self {
            time_low: 0xC12A7328u32.into(),
            time_mid: 0xF81Fu16.into(),
            time_hi_and_version: 0x11D2u16.into(),
            clock_seq_hi_and_reseved: 0xBAu8,
            clock_seq_loq: 0x4Bu8,
            node: [0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B]
        }
    }
}

#[repr(C, align(1))]
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct GPTPartition {
    pub part_type: Uuid,
    pub id: Uuid,
    pub first_lba: LittleEndian<u64>,
    pub last_lba: LittleEndian<u64>,
    pub attr: [u8; 8],
    pub name: [LittleEndian<u16>; 36],
}

#[repr(C, align(1))]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct GPTHeader {
    /// GPT header magic signature, hardcoded to "EFI PART".
    pub signature: [u8; 8], // Offset  0. "EFI PART", 45h 46h 49h 20h 50h 41h 52h 54h
    /// 00 00 01 00
    pub revision: LittleEndian<u32>, // Offset  8
    /// little endian
    pub header_size_le: LittleEndian<u32>, // Offset 12
    /// CRC32 of the header with crc32 section zeroed
    pub header_crc32: LittleEndian<u32>, // Offset 16
    /// must be 0
    pub reserved: LittleEndian<u32>, // Offset 20
    /// For main header, 1
    pub current_lba: LittleEndian<u64>, // Offset 24
    /// LBA for backup header
    pub backup_lba: LittleEndian<u64>, // Offset 32
    /// First usable LBA for partitions (primary table last LBA + 1)
    pub first_usable: LittleEndian<u64>, // Offset 40
    /// Last usable LBA (secondary partition table first LBA - 1)
    pub last_usable: LittleEndian<u64>, // Offset 48
    /// UUID of the disk
    pub disk_guid: Uuid, // Offset 56
    /// Starting LBA of partition entries
    pub part_start_lba: LittleEndian<u64>, // Offset 72
    /// Number of partition entries
    pub num_parts: LittleEndian<u32>, // Offset 80
    /// Size of a partition entry, usually 128
    pub part_size: LittleEndian<u32>, // Offset 84
    /// CRC32 of the partition table
    pub part_table_crc32: LittleEndian<u32>, // Offset 88
                                             // RESERVED: [u8; 96 - 512],
}

impl GPTPartition {
    pub unsafe fn partition_table_checksum(table: &[GPTPartition]) -> u32 {
        let table: &[u8] = mem::transmute(table);
        let hasher = Crc::<u32>::new(&crc::CRC_32_JAMCRC);
        hasher.checksum(table) ^ 0xffffffff
    }

    pub fn get_name(&self) -> Result<String, DecodeUtf16Error> {
        let mut ret = String::with_capacity(self.name.len());

        for ch in decode_utf16(self.name.iter().map(|c_le| -> u16 { c_le.clone().into() })) {
            match ch? {
                '\0' => break,
                x => ret.push(x),
            };
        }

        Ok(ret)
    }
}

#[repr(C)]
#[derive(Debug, Eq, PartialEq)]
pub struct LBA<T: Sized + LBAIndex>(T);

#[repr(C)]
pub struct LBA512([u8; 512]);

#[repr(C)]
pub struct LBA4K([u8; 4096]);

pub trait LBAIndex {
    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8];
}

impl LBAIndex for LBA512 {
    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
        &self.0[slice]
    }
}

impl LBAIndex for LBA4K {
    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
        &self.0[slice]
    }
}

impl<T: LBAIndex> Index<Range<usize>> for LBA<T> {
    type Output = [u8];
    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
        &self.0.index(slice)
    }
}

impl GPTHeader {
    pub fn verify_signature(&self) -> bool {
        const SIGNATURE: [u8; 8] = *b"EFI PART";
        self.signature == SIGNATURE
    }

    pub unsafe fn parse_gpt_header<'a, T: Sized + LBAIndex>(
        gpt_header_lba: &'a LBA<T>,
    ) -> Result<&'a Self, &'static str> {
        if size_of::<LBA<T>>() > isize::MAX as usize {
            return Err("Size of LBA block larger than the 32bit address space");
        }
        if size_of::<LBA<T>>() < 512 {
            return Err("Size of LBA smaller than 512 bytes");
        }

        let header = (gpt_header_lba as *const LBA<T>) as *const GPTHeader;
        let header = &(*header) as &GPTHeader;
        if !header.verify_signature() {
            return Err("Could not verify GPT header signature `EFI PART`");
        }

        let part_header_size = Into::<u32>::into(header.part_size) as usize;

        // Partition header size has to be 2^x * 128
        if part_header_size % 128 != 0 {
            return Err("Error in GPT table: Partition size not a multiple of 128");
        }
        if (part_header_size / 128).count_ones() != 1 {
            return Err("Error in GPT table: Partition size not a power of 2");
        }

        // Verify header
        assert_eq!(
            Self::header_checksum::<T>(&(*header)),
            header.header_crc32.into(),
            "GPT header CRC32 verification failed"
        );

        //FIXME: Check if there are enough LBAs to fit all partitions

        Ok(header) // Figure out if we can make this a reference instead of a copy
    }

    pub fn parse_partitions<F: Fn(&GPTPartition, &mut T), T>(
        part_array: &[GPTPartition],
        size: usize,
        foreach: F,
        data: &mut T,
    ) {
        //FIXME: Verify that part_header_size * num_parts does not overfloy beyond first_usable LBA
        let nil_uuid = Uuid::nil();

        // Size comes from the GPT Header and it is always a multiple of 128 bytes
        // which is the size of GPTPartition, we use it as a stride in our array
        let stride = size / size_of::<GPTPartition>();

        for i in 0..part_array.len() {
            if i % stride != 0 {
                continue;
            }
            if part_array[i].part_type == nil_uuid {
                continue;
            }
            foreach(&part_array[i], data);
        }
    }

    fn header_checksum<T: LBAIndex>(header: &GPTHeader) -> u32 {
        let verify_size = Into::<u32>::into(header.header_size_le) as usize;
        let mut verify_header: GPTHeader = *header;
        let v_header_block = (&verify_header as *const GPTHeader) as *const LBA<T>;
        verify_header.header_crc32 = 0.into();
        let hasher = Crc::<u32>::new(&crc::CRC_32_JAMCRC);
        (unsafe { hasher.checksum(&(*v_header_block)[0..verify_size]) }) ^ 0xffffffff
    }
}

#[cfg(test)]
#[cfg(not(feature = "no_std"))]
mod tests {
    extern crate base64;
    extern crate flate2;
    use base64::Engine;
    use explicit_endian::LittleEndian;

    use crate::{GPTHeader, GPTPartition, Uuid};
    use crate::{LBAIndex, LBA, LBA4K, LBA512};

    use std::io::Read;
    use std::vec::Vec;

    // This is a base64 encoded gzipped 100K disk image formatted for 512 bytes block size and two partitions
    const GPT_EXAMPLE1: &[u8] = b"H4sIAAAAAAAAA+3cPUgjQRQA4E1AOLAQezFi5d9ZWQgSwSKGEAsJCRwStBTFyoBgIQQMlnZqqVUOQtA+XRolVpJKjlME7YSzsEkjuQib9gqPHCd8Hwxv583beQNT7wYBn1k0+NVutyOdp6sPvJ2rJBZTI8sLmWwQRIJ8J/Pl549v7yuRsKK762gYy2GcfDx6fsilosX+6+HXfDoWDfPFcIyvr7x84ED8Yxfxy4H90nbycDe+cZvce8pOn9xUWom30/O72Xpiqtq999Ue9R8rTNRjreZgLR2cNeYbczsHhWzfZnroeGareZ9Zqq6FdeU/7gIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACfy0X8cmC/tJ083I1v3Cb3nrLTJzeVVuLt9Pxutp6Yqo6Gdas96j9WmKjHWs3BWjo4a8w35nYOCtm+zfTQ8cxW8z6zVF0L68o96g8AAAAAAAAAAAAAAAAAAAD/g8RiamR5IZMNgkiQ78wzX2ul9/xVuB4JY/c/AN3v8Ccfj54fcqlosf96+DWfjn0P88VwjK+vvPT+9Pyt3wAlEswAkAEA";

    // This is a base64 encoded gzipped 100K disk image formatted for 4Kbytes block size and two partitions
    const GPT_EXAMPLE_LBA4K: &[u8] = b"H4sICFR+fWIAA2xiYTRrLmltZwDt3L9LI0EUAOBdixSJaJErrA57aysJomBiCvEHURCsBRsRGwmiaLS4SrCwCoiV4DVaCEGwE2z8B0RMozYipFYQ1giTVkQu4sH3wTA7b97se+0Uu1HE/6wjaiRJEjefer5wevrvv+4HAACAn2kkX+ydGJoqRVEczTXXS5U/pfd4HPZbt8pUmLNhPuq8WV3pH62dZV6S58b+cEeIb4RxUdvda3/3AAAAwGcc5y67t7aXCzvl3MJ1Ye3hZOBqMb0+/npe3ZypHk7ete796W+qP1jPz9/3jc2edlXq5dTjQSbkZT98CwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAx7nL7q3t5cJOObdwXVh7OBm4Wkyvj7+eVzdnqoeTd6mQl/6m+oP1/Px939jsaVelXk49HmRCXrZN9QEAAAAAAAAAAAAAAAAAAOAnGMkXeyeGpkpRFEdzzfVt8en3e7wn7Mdhbv0HoPUd/lHnzepK/2jtLPOSPDf2h3+F+EYYF7XdvfZ3DwAAAHzGGwzc9x8AkAEA";

    fn test_disk_image<T: LBAIndex>(image: &[u8], uuid_time_low_1: u32, uuid_time_low_2: u32) {
        let base_decoded = base64::engine::general_purpose::STANDARD
            .decode(image)
            .unwrap();
        let mut disk_data: Vec<u8> = Vec::new();
        assert!(
            flate2::read::GzDecoder::new(&base_decoded[..])
                .read_to_end(&mut disk_data)
                .is_ok(),
            "flat2 could not gunzip disk blob"
        );

        // We hardcode LBA512 for the disk, typically you'd get this from the device
        let mbr_lba = disk_data.as_ptr() as *const LBA<T>;

        let gpt_header = unsafe { GPTHeader::parse_gpt_header(&*mbr_lba.add(1)) };
        assert!(
            gpt_header.is_ok(),
            "Could not parse GPT Header: {}",
            gpt_header.unwrap_err()
        );
        let gpt_header = gpt_header.unwrap();

        // This is the offset of LBA blocks starting from the GPT Header LBA
        let partition_lba_offset = Into::<u64>::into(gpt_header.part_start_lba) as usize;
        let partition_lba_ptr = unsafe { mbr_lba.add(partition_lba_offset) as *const GPTPartition };

        let part_size = Into::<u32>::into(gpt_header.part_size) as usize;
        let num_parts = Into::<u32>::into(gpt_header.num_parts) as usize;

        assert!(
            disk_data.len() > (std::mem::size_of::<LBA<T>>() * 2 + (part_size * num_parts)),
            "Partition table is bigger than buffer size"
        );

        let stride = part_size / std::mem::size_of::<GPTPartition>();
        let length = stride * num_parts;
        let part_array_ptr = core::ptr::slice_from_raw_parts(partition_lba_ptr, length);
        let part_array = unsafe { &*part_array_ptr };

        let checksum_array =
            core::ptr::slice_from_raw_parts(partition_lba_ptr, num_parts * part_size);
        let sum = unsafe { GPTPartition::partition_table_checksum(&*checksum_array) };

        assert_eq!(
            sum,
            Into::<u32>::into(gpt_header.part_table_crc32),
            "Bad CRC for partition array"
        );

        let mut partitions: Vec<GPTPartition> = Vec::new();
        GPTHeader::parse_partitions(
            part_array,
            Into::<u32>::into(gpt_header.part_size) as usize,
            |part, parts| {
                parts.push(part.clone());
            },
            &mut partitions,
        );

        assert_eq!(partitions.len(), 2, "List of partitions should have been 2");
        let guid_time_low = Into::<u32>::into(partitions[0].part_type.time_low);
        assert_eq!(
            guid_time_low, uuid_time_low_1,
            "First partition type did not match (time_low) expected value {:x}: {:x}",
            uuid_time_low_1, guid_time_low
        );
        let guid_time_low = Into::<u32>::into(partitions[1].part_type.time_low);
        assert_eq!(
            guid_time_low, uuid_time_low_2,
            "Second partition type did not match (time_low) expected value {:x}: {:x}",
            uuid_time_low_2, guid_time_low
        );
    }

    #[test]
    fn parse_512_header() {
        test_disk_image::<LBA512>(GPT_EXAMPLE1, 0x0fc63daf, 0xc12a7328);
    }

    #[test]
    fn parse_4k_header() {
        test_disk_image::<LBA4K>(GPT_EXAMPLE_LBA4K, 0x0fc63daf, 0x0fc63daf);
    }

    #[test]
    fn test_decode_name() {
        let v: Vec<u16> = "Hello".encode_utf16().collect();
        let mut v: Vec<LittleEndian<u16>> = v.iter().map(|v| v.clone().into()).collect();

        let mut part = GPTPartition {
            part_type: Uuid::nil(),
            id: Uuid::nil(),
            first_lba: 0.into(),
            last_lba: 0.into(),
            attr: unsafe { std::mem::zeroed() },
            name: unsafe { std::mem::zeroed() },
        };

        v.resize(part.name.len(), 0.into());
        part.name.copy_from_slice(&v);

        let name = part.get_name().unwrap();
        assert_eq!(name, "Hello");

        //TODO: test invalid UTF16
    }
}