Skip to main content

gpt_forensic/
entry.rs

1//! GPT partition entry parsing.
2//!
3//! Each entry is `partition_entry_size` bytes (spec value 128): a type GUID, a
4//! unique GUID, first/last LBA, attribute flags, and a 36-code-unit UTF-16LE
5//! name. An entry whose type GUID is all-zero is an unused slot.
6
7use crate::guid::Guid;
8use crate::Error;
9
10/// Minimum entry size the spec permits (and the default): 128 bytes.
11pub const MIN_ENTRY_SIZE: usize = 128;
12/// Byte offset of the UTF-16LE name within an entry.
13const NAME_OFFSET: usize = 56;
14/// Name length in bytes (36 UTF-16 code units).
15const NAME_LEN: usize = 72;
16
17/// A parsed GPT partition entry.
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize))]
20pub struct GptEntry {
21    /// Partition type GUID (all-zero = unused).
22    pub type_guid: Guid,
23    /// Unique partition GUID.
24    pub unique_guid: Guid,
25    /// First LBA of the partition (inclusive).
26    pub first_lba: u64,
27    /// Last LBA of the partition (inclusive).
28    pub last_lba: u64,
29    /// Attribute flags bitfield.
30    pub attributes: u64,
31    /// Partition name (decoded from UTF-16LE, trailing NULs stripped).
32    pub name: String,
33}
34
35fn u64_le(b: &[u8], off: usize) -> u64 {
36    let mut a = [0u8; 8];
37    a.copy_from_slice(&b[off..off + 8]);
38    u64::from_le_bytes(a)
39}
40
41impl GptEntry {
42    /// Parse one entry from the first 128 bytes of `bytes`.
43    ///
44    /// # Errors
45    /// [`Error::TooShort`] if `bytes` is under [`MIN_ENTRY_SIZE`].
46    pub fn parse(bytes: &[u8]) -> Result<GptEntry, Error> {
47        if bytes.len() < MIN_ENTRY_SIZE {
48            return Err(Error::TooShort {
49                need: MIN_ENTRY_SIZE,
50                got: bytes.len(),
51            });
52        }
53        let mut type_guid = [0u8; 16];
54        type_guid.copy_from_slice(&bytes[0..16]);
55        let mut unique_guid = [0u8; 16];
56        unique_guid.copy_from_slice(&bytes[16..32]);
57        Ok(GptEntry {
58            type_guid: Guid(type_guid),
59            unique_guid: Guid(unique_guid),
60            first_lba: u64_le(bytes, 32),
61            last_lba: u64_le(bytes, 40),
62            attributes: u64_le(bytes, 48),
63            name: decode_name(&bytes[NAME_OFFSET..NAME_OFFSET + NAME_LEN]),
64        })
65    }
66
67    /// `true` when the entry's type GUID is non-zero (an in-use partition).
68    #[must_use]
69    pub fn is_used(&self) -> bool {
70        !self.type_guid.is_zero()
71    }
72
73    /// Human-readable name for this partition's type GUID, from the
74    /// `forensicnomicon` knowledge base. `None` for an unrecognised type.
75    #[must_use]
76    pub fn type_name(&self) -> Option<&'static str> {
77        forensicnomicon::gpt::type_name(&self.type_guid.to_string())
78    }
79
80    /// Decode the entry's attribute flags into human-readable names (bit order),
81    /// e.g. `["hidden", "no-automount"]`. Uses the `forensicnomicon` knowledge base.
82    #[must_use]
83    pub fn attribute_names(&self) -> Vec<&'static str> {
84        forensicnomicon::gpt::attribute_names(self.attributes)
85    }
86}
87
88/// Decode a UTF-16LE name field, stopping at the first NUL code unit.
89fn decode_name(bytes: &[u8]) -> String {
90    let units: Vec<u16> = bytes
91        .chunks_exact(2)
92        .map(|c| u16::from_le_bytes([c[0], c[1]]))
93        .take_while(|&u| u != 0)
94        .collect();
95    String::from_utf16_lossy(&units)
96}
97
98/// Parse a partition-entry array, returning only the **used** entries.
99///
100/// `num_entries` and `entry_size` come from the GPT header. Entries are read at
101/// `entry_size` strides; a stride beyond the buffer ends iteration. Unused
102/// (all-zero type GUID) entries are skipped.
103#[must_use]
104pub fn parse_entry_array(array: &[u8], num_entries: u32, entry_size: u32) -> Vec<GptEntry> {
105    let stride = entry_size as usize;
106    if stride < MIN_ENTRY_SIZE {
107        return Vec::new();
108    }
109    let mut out = Vec::new();
110    for i in 0..num_entries as usize {
111        let start = i * stride;
112        let Some(slot) = array.get(start..start + MIN_ENTRY_SIZE) else {
113            break;
114        };
115        if let Ok(entry) = GptEntry::parse(slot) {
116            if entry.is_used() {
117                out.push(entry);
118            }
119        }
120    }
121    out
122}