gpt_parser/
lib.rs

1// Copyright 2022 Alberto Ruiz <aruiz@redhat.com>
2//
3// Use of this source code is governed by an MIT-style
4// license that can be found in the LICENSE file or at
5// https://opensource.org/licenses/MIT.
6
7#![cfg_attr(feature = "no_std", no_std)]
8
9use crc::Crc;
10
11use core::char::{decode_utf16, DecodeUtf16Error};
12
13#[cfg(feature = "no_std")]
14extern crate alloc;
15
16#[cfg(feature = "no_std")]
17use alloc::string::String;
18
19#[cfg(feature = "no_std")]
20use core::{mem, ops};
21
22#[cfg(not(feature = "no_std"))]
23use std::{mem, ops};
24
25use ops::{Index, Range};
26
27use mem::size_of;
28
29use explicit_endian::LittleEndian;
30
31#[repr(C, align(1))]
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub struct Uuid {
34    time_low: LittleEndian<u32>,
35    time_mid: LittleEndian<u16>,
36    time_hi_and_version: LittleEndian<u16>,
37    clock_seq_hi_and_reseved: u8,
38    clock_seq_loq: u8,
39    node: [u8; 6],
40}
41
42impl Uuid {
43    pub fn nil() -> Self {
44        Self {
45            time_low: 0.into(),
46            time_mid: 0.into(),
47            time_hi_and_version: 0.into(),
48            clock_seq_hi_and_reseved: 0,
49            clock_seq_loq: 0,
50            node: [0, 0, 0, 0, 0, 0],
51        }
52    }
53
54    pub fn efi() -> Self {
55        Self {
56            time_low: 0xC12A7328u32.into(),
57            time_mid: 0xF81Fu16.into(),
58            time_hi_and_version: 0x11D2u16.into(),
59            clock_seq_hi_and_reseved: 0xBAu8,
60            clock_seq_loq: 0x4Bu8,
61            node: [0x00, 0xA0, 0xC9, 0x3E, 0xC9, 0x3B]
62        }
63    }
64}
65
66#[repr(C, align(1))]
67#[derive(Debug, Clone, Copy, Eq, PartialEq)]
68pub struct GPTPartition {
69    pub part_type: Uuid,
70    pub id: Uuid,
71    pub first_lba: LittleEndian<u64>,
72    pub last_lba: LittleEndian<u64>,
73    pub attr: [u8; 8],
74    pub name: [LittleEndian<u16>; 36],
75}
76
77#[repr(C, align(1))]
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub struct GPTHeader {
80    /// GPT header magic signature, hardcoded to "EFI PART".
81    pub signature: [u8; 8], // Offset  0. "EFI PART", 45h 46h 49h 20h 50h 41h 52h 54h
82    /// 00 00 01 00
83    pub revision: LittleEndian<u32>, // Offset  8
84    /// little endian
85    pub header_size_le: LittleEndian<u32>, // Offset 12
86    /// CRC32 of the header with crc32 section zeroed
87    pub header_crc32: LittleEndian<u32>, // Offset 16
88    /// must be 0
89    pub reserved: LittleEndian<u32>, // Offset 20
90    /// For main header, 1
91    pub current_lba: LittleEndian<u64>, // Offset 24
92    /// LBA for backup header
93    pub backup_lba: LittleEndian<u64>, // Offset 32
94    /// First usable LBA for partitions (primary table last LBA + 1)
95    pub first_usable: LittleEndian<u64>, // Offset 40
96    /// Last usable LBA (secondary partition table first LBA - 1)
97    pub last_usable: LittleEndian<u64>, // Offset 48
98    /// UUID of the disk
99    pub disk_guid: Uuid, // Offset 56
100    /// Starting LBA of partition entries
101    pub part_start_lba: LittleEndian<u64>, // Offset 72
102    /// Number of partition entries
103    pub num_parts: LittleEndian<u32>, // Offset 80
104    /// Size of a partition entry, usually 128
105    pub part_size: LittleEndian<u32>, // Offset 84
106    /// CRC32 of the partition table
107    pub part_table_crc32: LittleEndian<u32>, // Offset 88
108                                             // RESERVED: [u8; 96 - 512],
109}
110
111impl GPTPartition {
112    pub unsafe fn partition_table_checksum(table: &[GPTPartition]) -> u32 {
113        let table: &[u8] = mem::transmute(table);
114        let hasher = Crc::<u32>::new(&crc::CRC_32_JAMCRC);
115        hasher.checksum(table) ^ 0xffffffff
116    }
117
118    pub fn get_name(&self) -> Result<String, DecodeUtf16Error> {
119        let mut ret = String::with_capacity(self.name.len());
120
121        for ch in decode_utf16(self.name.iter().map(|c_le| -> u16 { c_le.clone().into() })) {
122            match ch? {
123                '\0' => break,
124                x => ret.push(x),
125            };
126        }
127
128        Ok(ret)
129    }
130}
131
132#[repr(C)]
133#[derive(Debug, Eq, PartialEq)]
134pub struct LBA<T: Sized + LBAIndex>(T);
135
136#[repr(C)]
137pub struct LBA512([u8; 512]);
138
139#[repr(C)]
140pub struct LBA4K([u8; 4096]);
141
142pub trait LBAIndex {
143    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8];
144}
145
146impl LBAIndex for LBA512 {
147    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
148        &self.0[slice]
149    }
150}
151
152impl LBAIndex for LBA4K {
153    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
154        &self.0[slice]
155    }
156}
157
158impl<T: LBAIndex> Index<Range<usize>> for LBA<T> {
159    type Output = [u8];
160    fn index<'a>(&'a self, slice: Range<usize>) -> &'a [u8] {
161        &self.0.index(slice)
162    }
163}
164
165impl GPTHeader {
166    pub fn verify_signature(&self) -> bool {
167        const SIGNATURE: [u8; 8] = *b"EFI PART";
168        self.signature == SIGNATURE
169    }
170
171    pub unsafe fn parse_gpt_header<'a, T: Sized + LBAIndex>(
172        gpt_header_lba: &'a LBA<T>,
173    ) -> Result<&'a Self, &'static str> {
174        if size_of::<LBA<T>>() > isize::MAX as usize {
175            return Err("Size of LBA block larger than the 32bit address space");
176        }
177        if size_of::<LBA<T>>() < 512 {
178            return Err("Size of LBA smaller than 512 bytes");
179        }
180
181        let header = (gpt_header_lba as *const LBA<T>) as *const GPTHeader;
182        let header = &(*header) as &GPTHeader;
183        if !header.verify_signature() {
184            return Err("Could not verify GPT header signature `EFI PART`");
185        }
186
187        let part_header_size = Into::<u32>::into(header.part_size) as usize;
188
189        // Partition header size has to be 2^x * 128
190        if part_header_size % 128 != 0 {
191            return Err("Error in GPT table: Partition size not a multiple of 128");
192        }
193        if (part_header_size / 128).count_ones() != 1 {
194            return Err("Error in GPT table: Partition size not a power of 2");
195        }
196
197        // Verify header
198        assert_eq!(
199            Self::header_checksum::<T>(&(*header)),
200            header.header_crc32.into(),
201            "GPT header CRC32 verification failed"
202        );
203
204        //FIXME: Check if there are enough LBAs to fit all partitions
205
206        Ok(header) // Figure out if we can make this a reference instead of a copy
207    }
208
209    pub fn parse_partitions<F: Fn(&GPTPartition, &mut T), T>(
210        part_array: &[GPTPartition],
211        size: usize,
212        foreach: F,
213        data: &mut T,
214    ) {
215        //FIXME: Verify that part_header_size * num_parts does not overfloy beyond first_usable LBA
216        let nil_uuid = Uuid::nil();
217
218        // Size comes from the GPT Header and it is always a multiple of 128 bytes
219        // which is the size of GPTPartition, we use it as a stride in our array
220        let stride = size / size_of::<GPTPartition>();
221
222        for i in 0..part_array.len() {
223            if i % stride != 0 {
224                continue;
225            }
226            if part_array[i].part_type == nil_uuid {
227                continue;
228            }
229            foreach(&part_array[i], data);
230        }
231    }
232
233    fn header_checksum<T: LBAIndex>(header: &GPTHeader) -> u32 {
234        let verify_size = Into::<u32>::into(header.header_size_le) as usize;
235        let mut verify_header: GPTHeader = *header;
236        let v_header_block = (&verify_header as *const GPTHeader) as *const LBA<T>;
237        verify_header.header_crc32 = 0.into();
238        let hasher = Crc::<u32>::new(&crc::CRC_32_JAMCRC);
239        (unsafe { hasher.checksum(&(*v_header_block)[0..verify_size]) }) ^ 0xffffffff
240    }
241}
242
243#[cfg(test)]
244#[cfg(not(feature = "no_std"))]
245mod tests {
246    extern crate base64;
247    extern crate flate2;
248    use base64::Engine;
249    use explicit_endian::LittleEndian;
250
251    use crate::{GPTHeader, GPTPartition, Uuid};
252    use crate::{LBAIndex, LBA, LBA4K, LBA512};
253
254    use std::io::Read;
255    use std::vec::Vec;
256
257    // This is a base64 encoded gzipped 100K disk image formatted for 512 bytes block size and two partitions
258    const GPT_EXAMPLE1: &[u8] = b"H4sIAAAAAAAAA+3cPUgjQRQA4E1AOLAQezFi5d9ZWQgSwSKGEAsJCRwStBTFyoBgIQQMlnZqqVUOQtA+XRolVpJKjlME7YSzsEkjuQib9gqPHCd8Hwxv583beQNT7wYBn1k0+NVutyOdp6sPvJ2rJBZTI8sLmWwQRIJ8J/Pl549v7yuRsKK762gYy2GcfDx6fsilosX+6+HXfDoWDfPFcIyvr7x84ED8Yxfxy4H90nbycDe+cZvce8pOn9xUWom30/O72Xpiqtq999Ue9R8rTNRjreZgLR2cNeYbczsHhWzfZnroeGareZ9Zqq6FdeU/7gIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACfy0X8cmC/tJ083I1v3Cb3nrLTJzeVVuLt9Pxutp6Yqo6Gdas96j9WmKjHWs3BWjo4a8w35nYOCtm+zfTQ8cxW8z6zVF0L68o96g8AAAAAAAAAAAAAAAAAAAD/g8RiamR5IZMNgkiQ78wzX2ul9/xVuB4JY/c/AN3v8Ccfj54fcqlosf96+DWfjn0P88VwjK+vvPT+9Pyt3wAlEswAkAEA";
259
260    // This is a base64 encoded gzipped 100K disk image formatted for 4Kbytes block size and two partitions
261    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";
262
263    fn test_disk_image<T: LBAIndex>(image: &[u8], uuid_time_low_1: u32, uuid_time_low_2: u32) {
264        let base_decoded = base64::engine::general_purpose::STANDARD
265            .decode(image)
266            .unwrap();
267        let mut disk_data: Vec<u8> = Vec::new();
268        assert!(
269            flate2::read::GzDecoder::new(&base_decoded[..])
270                .read_to_end(&mut disk_data)
271                .is_ok(),
272            "flat2 could not gunzip disk blob"
273        );
274
275        // We hardcode LBA512 for the disk, typically you'd get this from the device
276        let mbr_lba = disk_data.as_ptr() as *const LBA<T>;
277
278        let gpt_header = unsafe { GPTHeader::parse_gpt_header(&*mbr_lba.add(1)) };
279        assert!(
280            gpt_header.is_ok(),
281            "Could not parse GPT Header: {}",
282            gpt_header.unwrap_err()
283        );
284        let gpt_header = gpt_header.unwrap();
285
286        // This is the offset of LBA blocks starting from the GPT Header LBA
287        let partition_lba_offset = Into::<u64>::into(gpt_header.part_start_lba) as usize;
288        let partition_lba_ptr = unsafe { mbr_lba.add(partition_lba_offset) as *const GPTPartition };
289
290        let part_size = Into::<u32>::into(gpt_header.part_size) as usize;
291        let num_parts = Into::<u32>::into(gpt_header.num_parts) as usize;
292
293        assert!(
294            disk_data.len() > (std::mem::size_of::<LBA<T>>() * 2 + (part_size * num_parts)),
295            "Partition table is bigger than buffer size"
296        );
297
298        let stride = part_size / std::mem::size_of::<GPTPartition>();
299        let length = stride * num_parts;
300        let part_array_ptr = core::ptr::slice_from_raw_parts(partition_lba_ptr, length);
301        let part_array = unsafe { &*part_array_ptr };
302
303        let checksum_array =
304            core::ptr::slice_from_raw_parts(partition_lba_ptr, num_parts * part_size);
305        let sum = unsafe { GPTPartition::partition_table_checksum(&*checksum_array) };
306
307        assert_eq!(
308            sum,
309            Into::<u32>::into(gpt_header.part_table_crc32),
310            "Bad CRC for partition array"
311        );
312
313        let mut partitions: Vec<GPTPartition> = Vec::new();
314        GPTHeader::parse_partitions(
315            part_array,
316            Into::<u32>::into(gpt_header.part_size) as usize,
317            |part, parts| {
318                parts.push(part.clone());
319            },
320            &mut partitions,
321        );
322
323        assert_eq!(partitions.len(), 2, "List of partitions should have been 2");
324        let guid_time_low = Into::<u32>::into(partitions[0].part_type.time_low);
325        assert_eq!(
326            guid_time_low, uuid_time_low_1,
327            "First partition type did not match (time_low) expected value {:x}: {:x}",
328            uuid_time_low_1, guid_time_low
329        );
330        let guid_time_low = Into::<u32>::into(partitions[1].part_type.time_low);
331        assert_eq!(
332            guid_time_low, uuid_time_low_2,
333            "Second partition type did not match (time_low) expected value {:x}: {:x}",
334            uuid_time_low_2, guid_time_low
335        );
336    }
337
338    #[test]
339    fn parse_512_header() {
340        test_disk_image::<LBA512>(GPT_EXAMPLE1, 0x0fc63daf, 0xc12a7328);
341    }
342
343    #[test]
344    fn parse_4k_header() {
345        test_disk_image::<LBA4K>(GPT_EXAMPLE_LBA4K, 0x0fc63daf, 0x0fc63daf);
346    }
347
348    #[test]
349    fn test_decode_name() {
350        let v: Vec<u16> = "Hello".encode_utf16().collect();
351        let mut v: Vec<LittleEndian<u16>> = v.iter().map(|v| v.clone().into()).collect();
352
353        let mut part = GPTPartition {
354            part_type: Uuid::nil(),
355            id: Uuid::nil(),
356            first_lba: 0.into(),
357            last_lba: 0.into(),
358            attr: unsafe { std::mem::zeroed() },
359            name: unsafe { std::mem::zeroed() },
360        };
361
362        v.resize(part.name.len(), 0.into());
363        part.name.copy_from_slice(&v);
364
365        let name = part.get_name().unwrap();
366        assert_eq!(name, "Hello");
367
368        //TODO: test invalid UTF16
369    }
370}