gpt/
lib.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{Context as _, Error, anyhow};
6use block_client::{BlockClient, BufferSlice, MutableBufferSlice, RemoteBlockClient};
7use fuchsia_sync::Mutex;
8use std::collections::BTreeMap;
9use std::sync::Arc;
10use zerocopy::{FromBytes as _, IntoBytes as _};
11
12pub mod format;
13
14/// GPT GUIDs are stored in mixed-endian format (see Appendix A of the EFI spec).  To ensure this is
15/// correctly handled, wrap the Uuid type to hide methods that use the UUIDs inappropriately.
16#[derive(Clone, Default, Debug)]
17pub struct Guid(uuid::Uuid);
18
19impl From<uuid::Uuid> for Guid {
20    fn from(uuid: uuid::Uuid) -> Self {
21        Self(uuid)
22    }
23}
24
25impl Guid {
26    pub fn from_bytes(bytes: [u8; 16]) -> Self {
27        Self(uuid::Uuid::from_bytes_le(bytes))
28    }
29
30    pub fn to_bytes(&self) -> [u8; 16] {
31        self.0.to_bytes_le()
32    }
33
34    pub fn to_string(&self) -> String {
35        self.0.to_string()
36    }
37
38    pub fn nil() -> Self {
39        Self(uuid::Uuid::nil())
40    }
41
42    pub fn generate() -> Self {
43        Self(uuid::Uuid::new_v4())
44    }
45}
46
47#[derive(Clone, Debug)]
48pub struct PartitionInfo {
49    pub label: String,
50    pub type_guid: Guid,
51    pub instance_guid: Guid,
52    pub start_block: u64,
53    pub num_blocks: u64,
54    pub flags: u64,
55}
56
57impl PartitionInfo {
58    pub fn from_entry(entry: &format::PartitionTableEntry) -> Result<Self, Error> {
59        let label = String::from_utf16(entry.name.split(|v| *v == 0u16).next().unwrap())?;
60        Ok(Self {
61            label,
62            type_guid: Guid::from_bytes(entry.type_guid),
63            instance_guid: Guid::from_bytes(entry.instance_guid),
64            start_block: entry.first_lba,
65            num_blocks: entry
66                .last_lba
67                .checked_add(1)
68                .unwrap()
69                .checked_sub(entry.first_lba)
70                .unwrap(),
71            flags: entry.flags,
72        })
73    }
74
75    pub fn as_entry(&self) -> format::PartitionTableEntry {
76        let mut name = [0u16; 36];
77        let raw = self.label.encode_utf16().collect::<Vec<_>>();
78        assert!(raw.len() <= name.len());
79        name[..raw.len()].copy_from_slice(&raw[..]);
80        format::PartitionTableEntry {
81            type_guid: self.type_guid.to_bytes(),
82            instance_guid: self.instance_guid.to_bytes(),
83            first_lba: self.start_block,
84            last_lba: self.start_block + self.num_blocks.saturating_sub(1),
85            flags: self.flags,
86            name,
87        }
88    }
89
90    pub fn nil() -> Self {
91        Self {
92            label: String::default(),
93            type_guid: Guid::default(),
94            instance_guid: Guid::default(),
95            start_block: 0,
96            num_blocks: 0,
97            flags: 0,
98        }
99    }
100
101    pub fn is_nil(&self) -> bool {
102        self.label == ""
103            && self.type_guid.0.is_nil()
104            && self.instance_guid.0.is_nil()
105            && self.start_block == 0
106            && self.num_blocks == 0
107            && self.flags == 0
108    }
109}
110
111enum WhichHeader {
112    Primary,
113    Backup,
114}
115
116impl WhichHeader {
117    fn offset(&self, block_size: u64, block_count: u64) -> u64 {
118        match self {
119            Self::Primary => block_size,
120            Self::Backup => (block_count - 1) * block_size,
121        }
122    }
123}
124
125async fn load_metadata(
126    client: &RemoteBlockClient,
127    which: WhichHeader,
128) -> Result<(format::Header, BTreeMap<u32, PartitionInfo>), Error> {
129    let bs = client.block_size() as usize;
130    let mut header_block = vec![0u8; client.block_size() as usize];
131    client
132        .read_at(
133            MutableBufferSlice::Memory(&mut header_block[..]),
134            which.offset(bs as u64, client.block_count() as u64),
135        )
136        .await
137        .context("Read header")?;
138    let (header, _) = format::Header::ref_from_prefix(&header_block[..])
139        .map_err(|_| anyhow!("Header has invalid size"))?;
140    header.ensure_integrity(client.block_count(), client.block_size() as u64)?;
141    let partition_table_offset = header.part_start * bs as u64;
142    let partition_table_size = (header.num_parts * header.part_size) as usize;
143    let partition_table_size_rounded = partition_table_size
144        .checked_next_multiple_of(bs)
145        .ok_or_else(|| anyhow!("Overflow when rounding up partition table size "))?;
146    let mut partition_table = BTreeMap::new();
147    if header.num_parts > 0 {
148        let mut partition_table_blocks = vec![0u8; partition_table_size_rounded];
149        client
150            .read_at(
151                MutableBufferSlice::Memory(&mut partition_table_blocks[..]),
152                partition_table_offset,
153            )
154            .await
155            .with_context(|| {
156                format!(
157                    "Failed to read partition table (sz {}) from offset {}",
158                    partition_table_size, partition_table_offset
159                )
160            })?;
161        let crc = crc::crc32::checksum_ieee(&partition_table_blocks[..partition_table_size]);
162        anyhow::ensure!(header.crc32_parts == crc, "Invalid partition table checksum");
163
164        for i in 0..header.num_parts as usize {
165            let entry_raw = &partition_table_blocks
166                [i * header.part_size as usize..(i + 1) * header.part_size as usize];
167            let (entry, _) = format::PartitionTableEntry::ref_from_prefix(entry_raw)
168                .map_err(|_| anyhow!("Failed to parse partition {i}"))?;
169            if entry.is_empty() {
170                continue;
171            }
172            entry.ensure_integrity().context("GPT partition table entry invalid!")?;
173
174            partition_table.insert(i as u32, PartitionInfo::from_entry(entry)?);
175        }
176    }
177    Ok((header.clone(), partition_table))
178}
179
180struct TransactionState {
181    pending_id: u64,
182    next_id: u64,
183}
184
185impl Default for TransactionState {
186    fn default() -> Self {
187        Self { pending_id: u64::MAX, next_id: 0 }
188    }
189}
190
191/// Manages a connection to a GPT-formatted block device.
192pub struct Gpt {
193    client: Arc<RemoteBlockClient>,
194    header: format::Header,
195    partitions: BTreeMap<u32, PartitionInfo>,
196    transaction_state: Arc<Mutex<TransactionState>>,
197}
198
199impl std::fmt::Debug for Gpt {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
201        f.debug_struct("Gpt")
202            .field("header", &self.header)
203            .field("partitions", &self.partitions)
204            .finish()
205    }
206}
207
208#[derive(Eq, thiserror::Error, Clone, Debug, PartialEq)]
209pub enum TransactionCommitError {
210    #[error("I/O error")]
211    Io,
212    #[error("Invalid arguments")]
213    InvalidArguments,
214    #[error("No space")]
215    NoSpace,
216}
217
218impl From<format::FormatError> for TransactionCommitError {
219    fn from(error: format::FormatError) -> Self {
220        match error {
221            format::FormatError::InvalidArguments => Self::InvalidArguments,
222            format::FormatError::NoSpace => Self::NoSpace,
223        }
224    }
225}
226
227impl From<TransactionCommitError> for zx::Status {
228    fn from(error: TransactionCommitError) -> zx::Status {
229        match error {
230            TransactionCommitError::Io => zx::Status::IO,
231            TransactionCommitError::InvalidArguments => zx::Status::INVALID_ARGS,
232            TransactionCommitError::NoSpace => zx::Status::NO_SPACE,
233        }
234    }
235}
236
237#[derive(Eq, thiserror::Error, Clone, Debug, PartialEq)]
238pub enum AddPartitionError {
239    #[error("Invalid arguments")]
240    InvalidArguments,
241    #[error("No space")]
242    NoSpace,
243}
244
245impl From<AddPartitionError> for zx::Status {
246    fn from(error: AddPartitionError) -> zx::Status {
247        match error {
248            AddPartitionError::InvalidArguments => zx::Status::INVALID_ARGS,
249            AddPartitionError::NoSpace => zx::Status::NO_SPACE,
250        }
251    }
252}
253
254impl Gpt {
255    /// Loads and validates a GPT-formatted block device.
256    pub async fn open(client: Arc<RemoteBlockClient>) -> Result<Self, Error> {
257        let mut restore_primary = false;
258        let (header, partitions) = match load_metadata(&client, WhichHeader::Primary).await {
259            Ok(v) => v,
260            Err(error) => {
261                log::warn!(error:?; "Failed to load primary metadata; falling back to backup");
262                restore_primary = true;
263                load_metadata(&client, WhichHeader::Backup)
264                    .await
265                    .context("Failed to load backup metadata")?
266            }
267        };
268        let mut this = Self {
269            client,
270            header,
271            partitions,
272            transaction_state: Arc::new(Mutex::new(TransactionState::default())),
273        };
274        if restore_primary {
275            log::info!("Restoring primary metadata from backup!");
276            this.header.backup_lba = this.header.current_lba;
277            this.header.current_lba = 1;
278            this.header.part_start = 2;
279            this.header.crc32 = this.header.compute_checksum();
280            let partition_table =
281                this.flattened_partitions().into_iter().map(|v| v.as_entry()).collect::<Vec<_>>();
282            let partition_table_raw = format::serialize_partition_table(
283                &mut this.header,
284                this.client.block_size() as usize,
285                this.client.block_count(),
286                &partition_table[..],
287            )
288            .context("Failed to serialize existing partition table")?;
289            this.write_metadata(&this.header, &partition_table_raw[..])
290                .await
291                .context("Failed to restore primary metadata")?;
292        }
293        Ok(this)
294    }
295
296    /// Formats `client` as a new GPT with `partitions`.  Overwrites any existing GPT on the block
297    /// device.
298    pub async fn format(
299        client: Arc<RemoteBlockClient>,
300        partitions: Vec<PartitionInfo>,
301    ) -> Result<Self, Error> {
302        let header = format::Header::new(
303            client.block_count(),
304            client.block_size(),
305            partitions.len() as u32,
306        )?;
307        let mut this = Self {
308            client,
309            header,
310            partitions: BTreeMap::new(),
311            transaction_state: Arc::new(Mutex::new(TransactionState::default())),
312        };
313        let mut transaction = this.create_transaction().unwrap();
314        transaction.partitions = partitions;
315        this.commit_transaction(transaction).await?;
316        Ok(this)
317    }
318
319    pub fn client(&self) -> &Arc<RemoteBlockClient> {
320        &self.client
321    }
322
323    #[cfg(test)]
324    fn take_client(self) -> Arc<RemoteBlockClient> {
325        self.client
326    }
327
328    pub fn header(&self) -> &format::Header {
329        &self.header
330    }
331
332    pub fn partitions(&self) -> &BTreeMap<u32, PartitionInfo> {
333        &self.partitions
334    }
335
336    // We only store valid partitions in memory.  This function allows us to flatten this back out
337    // to a non-sparse array for serialization.
338    fn flattened_partitions(&self) -> Vec<PartitionInfo> {
339        let mut partitions = vec![PartitionInfo::nil(); self.header.num_parts as usize];
340        for (idx, partition) in &self.partitions {
341            partitions[*idx as usize] = partition.clone();
342        }
343        partitions
344    }
345
346    /// Returns None if there's already a pending transaction.
347    pub fn create_transaction(&self) -> Option<Transaction> {
348        {
349            let mut state = self.transaction_state.lock();
350            if state.pending_id != u64::MAX {
351                return None;
352            } else {
353                state.pending_id = state.next_id;
354                state.next_id += 1;
355            }
356        }
357        Some(Transaction {
358            partitions: self.flattened_partitions(),
359            transaction_state: self.transaction_state.clone(),
360        })
361    }
362
363    pub async fn commit_transaction(
364        &mut self,
365        mut transaction: Transaction,
366    ) -> Result<(), TransactionCommitError> {
367        let mut new_header = self.header.clone();
368        let entries =
369            transaction.partitions.iter().map(|entry| entry.as_entry()).collect::<Vec<_>>();
370        let partition_table_raw = format::serialize_partition_table(
371            &mut new_header,
372            self.client.block_size() as usize,
373            self.client.block_count(),
374            &entries[..],
375        )?;
376
377        let mut backup_header = new_header.clone();
378        backup_header.current_lba = backup_header.backup_lba;
379        backup_header.backup_lba = 1;
380        backup_header.part_start = backup_header.last_usable + 1;
381        backup_header.crc32 = backup_header.compute_checksum();
382
383        // Per section 5.3.2 of the UEFI spec, the backup metadata must be written first.  The spec
384        // permits the partition table entries and header to be written in either order.
385        self.write_metadata(&backup_header, &partition_table_raw[..]).await.map_err(|err| {
386            log::warn!(err:?; "Failed to write metadata");
387            TransactionCommitError::Io
388        })?;
389        // NB: It would be preferable to use a barrier here, but not all drivers support barriers at
390        // this time.
391        // TODO(https://fxbug.dev/416348380): Use a barrier between writing secondary/primary.
392        self.client.flush().await.map_err(|err| {
393            log::warn!(err:?; "Failed to flush metadata writes");
394            TransactionCommitError::Io
395        })?;
396        self.write_metadata(&new_header, &partition_table_raw[..]).await.map_err(|err| {
397            log::warn!(err:?; "Failed to write metadata");
398            TransactionCommitError::Io
399        })?;
400        self.client.flush().await.map_err(|err| {
401            log::warn!(err:?; "Failed to flush metadata writes");
402            TransactionCommitError::Io
403        })?;
404
405        self.header = new_header;
406        self.partitions = BTreeMap::new();
407        let mut idx = 0;
408        for partition in std::mem::take(&mut transaction.partitions) {
409            if !partition.is_nil() {
410                self.partitions.insert(idx, partition);
411            }
412            idx += 1;
413        }
414        Ok(())
415    }
416
417    /// Adds a partition in `transaction`.  `info.start_block` must be unset and will be dynamically
418    /// chosen in a first-fit manner.
419    /// The indedx of the partition in the table is returned on success.
420    pub fn add_partition(
421        &mut self,
422        transaction: &mut Transaction,
423        mut info: PartitionInfo,
424    ) -> Result<usize, AddPartitionError> {
425        assert_eq!(info.start_block, 0);
426
427        if info.label.is_empty()
428            || info.type_guid.0.is_nil()
429            || info.instance_guid.0.is_nil()
430            || info.num_blocks == 0
431        {
432            return Err(AddPartitionError::InvalidArguments);
433        }
434
435        let mut allocated_ranges = vec![
436            0..self.header.first_usable,
437            self.header.last_usable + 1..self.client.block_count(),
438        ];
439        let mut slot_idx = None;
440        for i in 0..transaction.partitions.len() {
441            let partition = &transaction.partitions[i];
442            if slot_idx.is_none() && partition.is_nil() {
443                slot_idx = Some(i);
444            }
445            if !partition.is_nil() {
446                allocated_ranges
447                    .push(partition.start_block..partition.start_block + partition.num_blocks);
448            }
449        }
450        let slot_idx = slot_idx.ok_or(AddPartitionError::NoSpace)?;
451        allocated_ranges.sort_by_key(|range| range.start);
452
453        let mut start_block = None;
454        for window in allocated_ranges.windows(2) {
455            if window[1].start - window[0].end >= info.num_blocks {
456                start_block = Some(window[0].end);
457                break;
458            }
459        }
460        info.start_block = start_block.ok_or(AddPartitionError::NoSpace)?;
461
462        transaction.partitions[slot_idx] = info;
463        Ok(slot_idx)
464    }
465
466    async fn write_metadata(
467        &self,
468        header: &format::Header,
469        partition_table: &[u8],
470    ) -> Result<(), Error> {
471        let bs = self.client.block_size() as usize;
472        let mut header_block = vec![0u8; bs];
473        header.write_to_prefix(&mut header_block[..]).unwrap();
474        self.client
475            .write_at(BufferSlice::Memory(&header_block[..]), header.current_lba * bs as u64)
476            .await
477            .context("Failed to write header")?;
478        if !partition_table.is_empty() {
479            self.client
480                .write_at(BufferSlice::Memory(partition_table), header.part_start * bs as u64)
481                .await
482                .context("Failed to write partition table")?;
483        }
484        Ok(())
485    }
486}
487
488/// Pending changes to the GPT.
489pub struct Transaction {
490    pub partitions: Vec<PartitionInfo>,
491    transaction_state: Arc<Mutex<TransactionState>>,
492}
493
494impl std::fmt::Debug for Transaction {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
496        f.debug_struct("Transaction").field("partitions", &self.partitions).finish()
497    }
498}
499
500impl Drop for Transaction {
501    fn drop(&mut self) {
502        let mut state = self.transaction_state.lock();
503        debug_assert!(state.pending_id != u64::MAX);
504        state.pending_id = u64::MAX;
505    }
506}
507
508#[cfg(test)]
509mod tests {
510    use crate::{AddPartitionError, Gpt, Guid, PartitionInfo};
511    use block_client::{BlockClient as _, BufferSlice, MutableBufferSlice, RemoteBlockClient};
512    use std::ops::Range;
513    use std::sync::Arc;
514    use std::sync::atomic::{AtomicBool, Ordering};
515    use vmo_backed_block_server::{
516        InitialContents, VmoBackedServer, VmoBackedServerOptions, VmoBackedServerTestingExt as _,
517    };
518    use zx::HandleBased;
519    use {fidl_fuchsia_hardware_block_volume as fvolume, fuchsia_async as fasync};
520
521    #[fuchsia::test]
522    async fn load_unformatted_gpt() {
523        let vmo = zx::Vmo::create(4096).unwrap();
524        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
525        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
526
527        let _task =
528            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
529        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
530        Gpt::open(client).await.expect_err("load should fail");
531    }
532
533    #[fuchsia::test]
534    async fn load_formatted_empty_gpt() {
535        let vmo = zx::Vmo::create(4096).unwrap();
536        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
537        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
538
539        let _task =
540            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
541        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
542        Gpt::format(client.clone(), vec![]).await.expect("format failed");
543        Gpt::open(client).await.expect("load should succeed");
544    }
545
546    #[fuchsia::test]
547    async fn load_formatted_gpt_with_minimal_size() {
548        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
549        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
550        const PART_NAME: &str = "part";
551
552        let vmo = zx::Vmo::create(6 * 4096).unwrap();
553        let server = Arc::new(VmoBackedServer::from_vmo(4096, vmo));
554        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
555
556        let _task =
557            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
558        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
559        Gpt::format(
560            client.clone(),
561            vec![PartitionInfo {
562                label: PART_NAME.to_string(),
563                type_guid: Guid::from_bytes(PART_TYPE_GUID),
564                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
565                start_block: 3,
566                num_blocks: 1,
567                flags: 0,
568            }],
569        )
570        .await
571        .expect("format failed");
572        let manager = Gpt::open(client).await.expect("load should succeed");
573        assert_eq!(manager.header.first_usable, 3);
574        assert_eq!(manager.header.last_usable, 3);
575        let partition = manager.partitions().get(&0).expect("No entry found");
576        assert_eq!(partition.start_block, 3);
577        assert_eq!(partition.num_blocks, 1);
578        assert!(manager.partitions().get(&1).is_none());
579    }
580
581    #[fuchsia::test]
582    async fn load_formatted_gpt_with_one_partition() {
583        const PART_TYPE_GUID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
584        const PART_INSTANCE_GUID: [u8; 16] =
585            [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31];
586        const PART_NAME: &str = "part";
587
588        let vmo = zx::Vmo::create(4096).unwrap();
589        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
590        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
591
592        let _task =
593            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
594        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
595        Gpt::format(
596            client.clone(),
597            vec![PartitionInfo {
598                label: PART_NAME.to_string(),
599                type_guid: Guid::from_bytes(PART_TYPE_GUID),
600                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
601                start_block: 4,
602                num_blocks: 1,
603                flags: 0,
604            }],
605        )
606        .await
607        .expect("format failed");
608        let manager = Gpt::open(client).await.expect("load should succeed");
609        let partition = manager.partitions().get(&0).expect("No entry found");
610        assert_eq!(partition.label, "part");
611        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
612        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_GUID);
613        assert_eq!(partition.start_block, 4);
614        assert_eq!(partition.num_blocks, 1);
615        assert!(manager.partitions().get(&1).is_none());
616    }
617
618    #[fuchsia::test]
619    async fn load_formatted_gpt_with_two_partitions() {
620        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
621        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
622        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
623        const PART_1_NAME: &str = "part1";
624        const PART_2_NAME: &str = "part2";
625
626        let vmo = zx::Vmo::create(8192).unwrap();
627        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
628        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
629
630        let _task =
631            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
632        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
633        Gpt::format(
634            client.clone(),
635            vec![
636                PartitionInfo {
637                    label: PART_1_NAME.to_string(),
638                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
639                    instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
640                    start_block: 4,
641                    num_blocks: 1,
642                    flags: 0,
643                },
644                PartitionInfo {
645                    label: PART_2_NAME.to_string(),
646                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
647                    instance_guid: Guid::from_bytes(PART_INSTANCE_2_GUID),
648                    start_block: 7,
649                    num_blocks: 1,
650                    flags: 0,
651                },
652            ],
653        )
654        .await
655        .expect("format failed");
656        let manager = Gpt::open(client).await.expect("load should succeed");
657        let partition = manager.partitions().get(&0).expect("No entry found");
658        assert_eq!(partition.label, PART_1_NAME);
659        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
660        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
661        assert_eq!(partition.start_block, 4);
662        assert_eq!(partition.num_blocks, 1);
663        let partition = manager.partitions().get(&1).expect("No entry found");
664        assert_eq!(partition.label, PART_2_NAME);
665        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
666        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
667        assert_eq!(partition.start_block, 7);
668        assert_eq!(partition.num_blocks, 1);
669        assert!(manager.partitions().get(&2).is_none());
670    }
671
672    #[fuchsia::test]
673    async fn load_formatted_gpt_with_extra_bytes_in_partition_name() {
674        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
675        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
676        const PART_NAME: &str = "part\0extrastuff";
677
678        let vmo = zx::Vmo::create(4096).unwrap();
679        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
680        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
681
682        let _task =
683            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
684        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
685        Gpt::format(
686            client.clone(),
687            vec![PartitionInfo {
688                label: PART_NAME.to_string(),
689                type_guid: Guid::from_bytes(PART_TYPE_GUID),
690                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
691                start_block: 4,
692                num_blocks: 1,
693                flags: 0,
694            }],
695        )
696        .await
697        .expect("format failed");
698        let manager = Gpt::open(client).await.expect("load should succeed");
699        let partition = manager.partitions().get(&0).expect("No entry found");
700        // The name should have everything after the first nul byte stripped.
701        assert_eq!(partition.label, "part");
702    }
703
704    #[fuchsia::test]
705    async fn load_formatted_gpt_with_empty_partition_name() {
706        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
707        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
708        const PART_NAME: &str = "";
709
710        let vmo = zx::Vmo::create(4096).unwrap();
711        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
712        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
713
714        let _task =
715            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
716        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
717        Gpt::format(
718            client.clone(),
719            vec![PartitionInfo {
720                label: PART_NAME.to_string(),
721                type_guid: Guid::from_bytes(PART_TYPE_GUID),
722                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
723                start_block: 4,
724                num_blocks: 1,
725                flags: 0,
726            }],
727        )
728        .await
729        .expect("format failed");
730        let manager = Gpt::open(client).await.expect("load should succeed");
731        let partition = manager.partitions().get(&0).expect("No entry found");
732        assert_eq!(partition.label, "");
733    }
734
735    #[fuchsia::test]
736    async fn load_formatted_gpt_with_invalid_primary_header() {
737        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
738        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
739        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
740        const PART_1_NAME: &str = "part1";
741        const PART_2_NAME: &str = "part2";
742
743        let vmo = zx::Vmo::create(8192).unwrap();
744
745        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
746        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
747
748        let _task =
749            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
750        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
751        Gpt::format(
752            client.clone(),
753            vec![
754                PartitionInfo {
755                    label: PART_1_NAME.to_string(),
756                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
757                    instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
758                    start_block: 4,
759                    num_blocks: 1,
760                    flags: 0,
761                },
762                PartitionInfo {
763                    label: PART_2_NAME.to_string(),
764                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
765                    instance_guid: Guid::from_bytes(PART_INSTANCE_2_GUID),
766                    start_block: 7,
767                    num_blocks: 1,
768                    flags: 0,
769                },
770            ],
771        )
772        .await
773        .expect("format failed");
774        // Clobber the primary header.  The backup should allow the GPT to be used.
775        client.write_at(BufferSlice::Memory(&[0xffu8; 512]), 512).await.unwrap();
776        let manager = Gpt::open(client).await.expect("load should succeed");
777        let partition = manager.partitions().get(&0).expect("No entry found");
778        assert_eq!(partition.label, PART_1_NAME);
779        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
780        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
781        assert_eq!(partition.start_block, 4);
782        assert_eq!(partition.num_blocks, 1);
783        let partition = manager.partitions().get(&1).expect("No entry found");
784        assert_eq!(partition.label, PART_2_NAME);
785        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
786        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
787        assert_eq!(partition.start_block, 7);
788        assert_eq!(partition.num_blocks, 1);
789        assert!(manager.partitions().get(&2).is_none());
790    }
791
792    #[fuchsia::test]
793    async fn load_formatted_gpt_with_invalid_primary_partition_table() {
794        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
795        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
796        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
797        const PART_1_NAME: &str = "part1";
798        const PART_2_NAME: &str = "part2";
799
800        let vmo = zx::Vmo::create(8192).unwrap();
801
802        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
803        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
804
805        let _task =
806            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
807        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
808        Gpt::format(
809            client.clone(),
810            vec![
811                PartitionInfo {
812                    label: PART_1_NAME.to_string(),
813                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
814                    instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
815                    start_block: 4,
816                    num_blocks: 1,
817                    flags: 0,
818                },
819                PartitionInfo {
820                    label: PART_2_NAME.to_string(),
821                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
822                    instance_guid: Guid::from_bytes(PART_INSTANCE_2_GUID),
823                    start_block: 7,
824                    num_blocks: 1,
825                    flags: 0,
826                },
827            ],
828        )
829        .await
830        .expect("format failed");
831        // Clobber the primary partition table.  The backup should allow the GPT to be used.
832        client.write_at(BufferSlice::Memory(&[0xffu8; 512]), 1024).await.unwrap();
833        let manager = Gpt::open(client).await.expect("load should succeed");
834        let partition = manager.partitions().get(&0).expect("No entry found");
835        assert_eq!(partition.label, PART_1_NAME);
836        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
837        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
838        assert_eq!(partition.start_block, 4);
839        assert_eq!(partition.num_blocks, 1);
840        let partition = manager.partitions().get(&1).expect("No entry found");
841        assert_eq!(partition.label, PART_2_NAME);
842        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
843        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
844        assert_eq!(partition.start_block, 7);
845        assert_eq!(partition.num_blocks, 1);
846        assert!(manager.partitions().get(&2).is_none());
847    }
848
849    #[fuchsia::test]
850    async fn drop_transaction() {
851        let vmo = zx::Vmo::create(8192).unwrap();
852        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
853        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
854
855        let _task =
856            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
857        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
858        Gpt::format(client.clone(), vec![]).await.expect("format failed");
859        let manager = Gpt::open(client).await.expect("load should succeed");
860        {
861            let _transaction = manager.create_transaction().unwrap();
862            assert!(manager.create_transaction().is_none());
863        }
864        let _transaction =
865            manager.create_transaction().expect("Transaction dropped but not available");
866    }
867
868    #[fuchsia::test]
869    async fn commit_empty_transaction() {
870        let vmo = zx::Vmo::create(8192).unwrap();
871        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
872        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
873
874        let _task =
875            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
876        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
877        Gpt::format(client.clone(), vec![]).await.expect("format failed");
878        let mut manager = Gpt::open(client).await.expect("load should succeed");
879        let transaction = manager.create_transaction().unwrap();
880        manager.commit_transaction(transaction).await.expect("Commit failed");
881
882        // Check state before and after a reload, to ensure both the in-memory and on-disk
883        // representation match.
884        assert_eq!(manager.header().num_parts, 0);
885        assert!(manager.partitions().is_empty());
886        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
887        assert_eq!(manager.header().num_parts, 0);
888        assert!(manager.partitions().is_empty());
889    }
890
891    #[fuchsia::test]
892    async fn add_partition_in_transaction() {
893        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
894        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
895        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
896        const PART_1_NAME: &str = "part1";
897        const PART_2_NAME: &str = "part2";
898
899        let vmo = zx::Vmo::create(8192).unwrap();
900        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
901        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
902
903        let _task =
904            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
905        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
906        Gpt::format(
907            client.clone(),
908            vec![PartitionInfo {
909                label: PART_1_NAME.to_string(),
910                type_guid: Guid::from_bytes(PART_TYPE_GUID),
911                instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
912                start_block: 4,
913                num_blocks: 1,
914                flags: 0,
915            }],
916        )
917        .await
918        .expect("format failed");
919        let mut manager = Gpt::open(client).await.expect("load should succeed");
920        let mut transaction = manager.create_transaction().unwrap();
921        assert_eq!(transaction.partitions.len(), 1);
922        transaction.partitions.push(crate::PartitionInfo {
923            label: PART_2_NAME.to_string(),
924            type_guid: crate::Guid::from_bytes(PART_TYPE_GUID),
925            instance_guid: crate::Guid::from_bytes(PART_INSTANCE_2_GUID),
926            start_block: 7,
927            num_blocks: 1,
928            flags: 0,
929        });
930        manager.commit_transaction(transaction).await.expect("Commit failed");
931
932        // Check state before and after a reload, to ensure both the in-memory and on-disk
933        // representation match.
934        assert_eq!(manager.header().num_parts, 2);
935        assert!(manager.partitions().get(&2).is_none());
936        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
937        assert_eq!(manager.header().num_parts, 2);
938        let partition = manager.partitions().get(&0).expect("No entry found");
939        assert_eq!(partition.label, PART_1_NAME);
940        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
941        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
942        assert_eq!(partition.start_block, 4);
943        assert_eq!(partition.num_blocks, 1);
944        let partition = manager.partitions().get(&1).expect("No entry found");
945        assert_eq!(partition.label, PART_2_NAME);
946        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
947        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
948        assert_eq!(partition.start_block, 7);
949        assert_eq!(partition.num_blocks, 1);
950        assert!(manager.partitions().get(&2).is_none());
951    }
952
953    #[fuchsia::test]
954    async fn remove_partition_in_transaction() {
955        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
956        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
957        const PART_NAME: &str = "part1";
958
959        let vmo = zx::Vmo::create(8192).unwrap();
960        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
961        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
962
963        let _task =
964            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
965        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
966        Gpt::format(
967            client.clone(),
968            vec![PartitionInfo {
969                label: PART_NAME.to_string(),
970                type_guid: Guid::from_bytes(PART_TYPE_GUID),
971                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
972                start_block: 4,
973                num_blocks: 1,
974                flags: 0,
975            }],
976        )
977        .await
978        .expect("format failed");
979        let mut manager = Gpt::open(client).await.expect("load should succeed");
980        let mut transaction = manager.create_transaction().unwrap();
981        assert_eq!(transaction.partitions.len(), 1);
982        transaction.partitions.clear();
983        manager.commit_transaction(transaction).await.expect("Commit failed");
984
985        // Check state before and after a reload, to ensure both the in-memory and on-disk
986        // representation match.
987        assert_eq!(manager.header().num_parts, 0);
988        assert!(manager.partitions().get(&0).is_none());
989        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
990        assert_eq!(manager.header().num_parts, 0);
991        assert!(manager.partitions().get(&0).is_none());
992    }
993
994    #[fuchsia::test]
995    async fn modify_partition_in_transaction() {
996        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
997        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
998        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
999        const PART_1_NAME: &str = "part1";
1000        const PART_2_NAME: &str = "part2";
1001
1002        let vmo = zx::Vmo::create(8192).unwrap();
1003        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1004        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1005
1006        let _task =
1007            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1008        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1009        Gpt::format(
1010            client.clone(),
1011            vec![PartitionInfo {
1012                label: PART_1_NAME.to_string(),
1013                type_guid: Guid::from_bytes(PART_TYPE_GUID),
1014                instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
1015                start_block: 4,
1016                num_blocks: 1,
1017                flags: 0,
1018            }],
1019        )
1020        .await
1021        .expect("format failed");
1022        let mut manager = Gpt::open(client).await.expect("load should succeed");
1023        let mut transaction = manager.create_transaction().unwrap();
1024        assert_eq!(transaction.partitions.len(), 1);
1025        transaction.partitions[0] = crate::PartitionInfo {
1026            label: PART_2_NAME.to_string(),
1027            type_guid: crate::Guid::from_bytes(PART_TYPE_GUID),
1028            instance_guid: crate::Guid::from_bytes(PART_INSTANCE_2_GUID),
1029            start_block: 7,
1030            num_blocks: 1,
1031            flags: 0,
1032        };
1033        manager.commit_transaction(transaction).await.expect("Commit failed");
1034
1035        // Check state before and after a reload, to ensure both the in-memory and on-disk
1036        // representation match.
1037        assert_eq!(manager.header().num_parts, 1);
1038        let partition = manager.partitions().get(&0).expect("No entry found");
1039        assert_eq!(partition.label, PART_2_NAME);
1040        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1041        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
1042        assert_eq!(partition.start_block, 7);
1043        assert_eq!(partition.num_blocks, 1);
1044        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1045        assert_eq!(manager.header().num_parts, 1);
1046        let partition = manager.partitions().get(&0).expect("No entry found");
1047        assert_eq!(partition.label, PART_2_NAME);
1048        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1049        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
1050        assert_eq!(partition.start_block, 7);
1051        assert_eq!(partition.num_blocks, 1);
1052        assert!(manager.partitions().get(&1).is_none());
1053    }
1054
1055    #[fuchsia::test]
1056    async fn grow_partition_table_in_transaction() {
1057        let vmo = zx::Vmo::create(1024 * 1024).unwrap();
1058        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1059        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1060
1061        let _task =
1062            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1063        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1064        Gpt::format(
1065            client.clone(),
1066            vec![PartitionInfo {
1067                label: "part".to_string(),
1068                type_guid: Guid::from_bytes([1u8; 16]),
1069                instance_guid: Guid::from_bytes([1u8; 16]),
1070                start_block: 34,
1071                num_blocks: 1,
1072                flags: 0,
1073            }],
1074        )
1075        .await
1076        .expect("format failed");
1077        let mut manager = Gpt::open(client).await.expect("load should succeed");
1078        assert_eq!(manager.header().num_parts, 1);
1079        assert_eq!(manager.header().first_usable, 3);
1080        let mut transaction = manager.create_transaction().unwrap();
1081        transaction.partitions.resize(128, crate::PartitionInfo::nil());
1082        manager.commit_transaction(transaction).await.expect("Commit failed");
1083
1084        // Check state before and after a reload, to ensure both the in-memory and on-disk
1085        // representation match.
1086        assert_eq!(manager.header().num_parts, 128);
1087        assert_eq!(manager.header().first_usable, 34);
1088        let partition = manager.partitions().get(&0).expect("No entry found");
1089        assert_eq!(partition.label, "part");
1090        assert_eq!(partition.type_guid.to_bytes(), [1u8; 16]);
1091        assert_eq!(partition.instance_guid.to_bytes(), [1u8; 16]);
1092        assert_eq!(partition.start_block, 34);
1093        assert_eq!(partition.num_blocks, 1);
1094        assert!(manager.partitions().get(&1).is_none());
1095        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1096        assert_eq!(manager.header().num_parts, 128);
1097        assert_eq!(manager.header().first_usable, 34);
1098        let partition = manager.partitions().get(&0).expect("No entry found");
1099        assert_eq!(partition.label, "part");
1100        assert_eq!(partition.type_guid.to_bytes(), [1u8; 16]);
1101        assert_eq!(partition.instance_guid.to_bytes(), [1u8; 16]);
1102        assert_eq!(partition.start_block, 34);
1103        assert_eq!(partition.num_blocks, 1);
1104        assert!(manager.partitions().get(&1).is_none());
1105    }
1106
1107    #[fuchsia::test]
1108    async fn shrink_partition_table_in_transaction() {
1109        let vmo = zx::Vmo::create(1024 * 1024).unwrap();
1110        let mut partitions = vec![];
1111        for i in 0..128 {
1112            partitions.push(PartitionInfo {
1113                label: format!("part-{i}"),
1114                type_guid: Guid::from_bytes([i as u8 + 1; 16]),
1115                instance_guid: Guid::from_bytes([i as u8 + 1; 16]),
1116                start_block: 34 + i,
1117                num_blocks: 1,
1118                flags: 0,
1119            });
1120        }
1121        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1122        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1123
1124        let _task =
1125            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1126        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1127        Gpt::format(client.clone(), partitions).await.expect("format failed");
1128        let mut manager = Gpt::open(client).await.expect("load should succeed");
1129        assert_eq!(manager.header().num_parts, 128);
1130        assert_eq!(manager.header().first_usable, 34);
1131        let mut transaction = manager.create_transaction().unwrap();
1132        transaction.partitions.clear();
1133        manager.commit_transaction(transaction).await.expect("Commit failed");
1134
1135        // Check state before and after a reload, to ensure both the in-memory and on-disk
1136        // representation match.
1137        assert_eq!(manager.header().num_parts, 0);
1138        assert_eq!(manager.header().first_usable, 2);
1139        assert!(manager.partitions().get(&0).is_none());
1140        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1141        assert_eq!(manager.header().num_parts, 0);
1142        assert_eq!(manager.header().first_usable, 2);
1143        assert!(manager.partitions().get(&0).is_none());
1144    }
1145
1146    #[fuchsia::test]
1147    async fn invalid_transaction_rejected() {
1148        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
1149        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
1150        const PART_NAME: &str = "part1";
1151
1152        let vmo = zx::Vmo::create(8192).unwrap();
1153        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1154        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1155
1156        let _task =
1157            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1158        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1159        Gpt::format(
1160            client.clone(),
1161            vec![PartitionInfo {
1162                label: PART_NAME.to_string(),
1163                type_guid: Guid::from_bytes(PART_TYPE_GUID),
1164                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
1165                start_block: 4,
1166                num_blocks: 1,
1167                flags: 0,
1168            }],
1169        )
1170        .await
1171        .expect("format failed");
1172        let mut manager = Gpt::open(client).await.expect("load should succeed");
1173        let mut transaction = manager.create_transaction().unwrap();
1174        assert_eq!(transaction.partitions.len(), 1);
1175        // This overlaps with the GPT metadata, so is invalid.
1176        transaction.partitions[0].start_block = 0;
1177        manager.commit_transaction(transaction).await.expect_err("Commit should have failed");
1178
1179        // Ensure nothing changed. Check state before and after a reload, to ensure both the
1180        // in-memory and on-disk representation match.
1181        assert_eq!(manager.header().num_parts, 1);
1182        let partition = manager.partitions().get(&0).expect("No entry found");
1183        assert_eq!(partition.label, PART_NAME);
1184        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1185        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_GUID);
1186        assert_eq!(partition.start_block, 4);
1187        assert_eq!(partition.num_blocks, 1);
1188        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1189        assert_eq!(manager.header().num_parts, 1);
1190        let partition = manager.partitions().get(&0).expect("No entry found");
1191        assert_eq!(partition.label, PART_NAME);
1192        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1193        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_GUID);
1194        assert_eq!(partition.start_block, 4);
1195        assert_eq!(partition.num_blocks, 1);
1196    }
1197
1198    /// An Observer that discards all writes overlapping its range (specified in bytes, not blocks).
1199    struct DiscardingObserver {
1200        block_size: u64,
1201        discard_range: Range<u64>,
1202    }
1203
1204    impl vmo_backed_block_server::Observer for DiscardingObserver {
1205        fn write(
1206            &self,
1207            device_block_offset: u64,
1208            block_count: u32,
1209            _vmo: &Arc<zx::Vmo>,
1210            _vmo_offset: u64,
1211            _opts: block_server::WriteOptions,
1212        ) -> vmo_backed_block_server::WriteAction {
1213            let write_range = (device_block_offset * self.block_size)
1214                ..(device_block_offset + block_count as u64) * self.block_size;
1215            if write_range.end <= self.discard_range.start
1216                || write_range.start >= self.discard_range.end
1217            {
1218                vmo_backed_block_server::WriteAction::Write
1219            } else {
1220                vmo_backed_block_server::WriteAction::Discard
1221            }
1222        }
1223    }
1224
1225    #[fuchsia::test]
1226    async fn transaction_applied_if_primary_metadata_partially_written() {
1227        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
1228        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
1229        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
1230        const PART_1_NAME: &str = "part1";
1231        const PART_2_NAME: &str = "part2";
1232
1233        let vmo = zx::Vmo::create(8192).unwrap();
1234        let server = Arc::new(
1235            VmoBackedServerOptions {
1236                initial_contents: InitialContents::FromVmo(vmo),
1237                block_size: 512,
1238                observer: Some(Box::new(DiscardingObserver {
1239                    discard_range: 1024..1536,
1240                    block_size: 512,
1241                })),
1242                ..Default::default()
1243            }
1244            .build()
1245            .unwrap(),
1246        );
1247        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1248
1249        let _task =
1250            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1251        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1252        Gpt::format(
1253            client.clone(),
1254            vec![PartitionInfo {
1255                label: PART_1_NAME.to_string(),
1256                type_guid: Guid::from_bytes(PART_TYPE_GUID),
1257                instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
1258                start_block: 4,
1259                num_blocks: 1,
1260                flags: 0,
1261            }],
1262        )
1263        .await
1264        .expect("format failed");
1265        let mut manager = Gpt::open(client).await.expect("load should succeed");
1266        let mut transaction = manager.create_transaction().unwrap();
1267        transaction.partitions.push(crate::PartitionInfo {
1268            label: PART_2_NAME.to_string(),
1269            type_guid: crate::Guid::from_bytes(PART_TYPE_GUID),
1270            instance_guid: crate::Guid::from_bytes(PART_INSTANCE_2_GUID),
1271            start_block: 7,
1272            num_blocks: 1,
1273            flags: 0,
1274        });
1275        manager.commit_transaction(transaction).await.expect("Commit failed");
1276
1277        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1278        assert_eq!(manager.header().num_parts, 2);
1279        let partition = manager.partitions().get(&0).expect("No entry found");
1280        assert_eq!(partition.label, PART_1_NAME);
1281        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1282        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
1283        assert_eq!(partition.start_block, 4);
1284        assert_eq!(partition.num_blocks, 1);
1285        let partition = manager.partitions().get(&1).expect("No entry found");
1286        assert_eq!(partition.label, PART_2_NAME);
1287        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1288        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_2_GUID);
1289        assert_eq!(partition.start_block, 7);
1290        assert_eq!(partition.num_blocks, 1);
1291    }
1292
1293    #[fuchsia::test]
1294    async fn transaction_not_applied_if_primary_metadata_not_written() {
1295        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
1296        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
1297        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
1298        const PART_1_NAME: &str = "part1";
1299        const PART_2_NAME: &str = "part2";
1300
1301        let vmo = zx::Vmo::create(8192).unwrap();
1302        let vmo_dup = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
1303        {
1304            let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1305            let server = Arc::new(VmoBackedServer::from_vmo(512, vmo_dup));
1306            let _task =
1307                fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1308            let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1309            Gpt::format(
1310                client.clone(),
1311                vec![PartitionInfo {
1312                    label: PART_1_NAME.to_string(),
1313                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
1314                    instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
1315                    start_block: 4,
1316                    num_blocks: 1,
1317                    flags: 0,
1318                }],
1319            )
1320            .await
1321            .expect("format failed");
1322        }
1323        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1324        let server = Arc::new(
1325            VmoBackedServerOptions {
1326                initial_contents: InitialContents::FromVmo(vmo),
1327                block_size: 512,
1328                observer: Some(Box::new(DiscardingObserver {
1329                    discard_range: 0..2048,
1330                    block_size: 512,
1331                })),
1332                ..Default::default()
1333            }
1334            .build()
1335            .unwrap(),
1336        );
1337        let _task =
1338            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1339        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1340
1341        let mut manager = Gpt::open(client).await.expect("load should succeed");
1342        let mut transaction = manager.create_transaction().unwrap();
1343        transaction.partitions.push(crate::PartitionInfo {
1344            label: PART_2_NAME.to_string(),
1345            type_guid: crate::Guid::from_bytes(PART_TYPE_GUID),
1346            instance_guid: crate::Guid::from_bytes(PART_INSTANCE_2_GUID),
1347            start_block: 7,
1348            num_blocks: 1,
1349            flags: 0,
1350        });
1351        manager.commit_transaction(transaction).await.expect("Commit failed");
1352
1353        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1354        assert_eq!(manager.header().num_parts, 1);
1355        let partition = manager.partitions().get(&0).expect("No entry found");
1356        assert_eq!(partition.label, PART_1_NAME);
1357        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1358        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
1359        assert_eq!(partition.start_block, 4);
1360        assert_eq!(partition.num_blocks, 1);
1361        assert!(manager.partitions().get(&1).is_none());
1362    }
1363
1364    #[fuchsia::test]
1365    async fn transaction_not_applied_if_backup_metadata_partially_written() {
1366        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
1367        const PART_INSTANCE_1_GUID: [u8; 16] = [2u8; 16];
1368        const PART_INSTANCE_2_GUID: [u8; 16] = [3u8; 16];
1369        const PART_1_NAME: &str = "part1";
1370        const PART_2_NAME: &str = "part2";
1371
1372        let vmo = zx::Vmo::create(8192).unwrap();
1373        let vmo_dup = vmo.duplicate_handle(zx::Rights::SAME_RIGHTS).unwrap();
1374        {
1375            let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1376            let server = Arc::new(VmoBackedServer::from_vmo(512, vmo_dup));
1377            let _task =
1378                fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1379            let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1380            Gpt::format(
1381                client.clone(),
1382                vec![PartitionInfo {
1383                    label: PART_1_NAME.to_string(),
1384                    type_guid: Guid::from_bytes(PART_TYPE_GUID),
1385                    instance_guid: Guid::from_bytes(PART_INSTANCE_1_GUID),
1386                    start_block: 4,
1387                    num_blocks: 1,
1388                    flags: 0,
1389                }],
1390            )
1391            .await
1392            .expect("format failed");
1393        }
1394        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1395        let server = Arc::new(
1396            VmoBackedServerOptions {
1397                initial_contents: InitialContents::FromVmo(vmo),
1398                block_size: 512,
1399                observer: Some(Box::new(DiscardingObserver {
1400                    discard_range: 0..7680,
1401                    block_size: 512,
1402                })),
1403                ..Default::default()
1404            }
1405            .build()
1406            .unwrap(),
1407        );
1408        let _task =
1409            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1410        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1411
1412        let mut manager = Gpt::open(client).await.expect("load should succeed");
1413        let mut transaction = manager.create_transaction().unwrap();
1414        transaction.partitions.push(crate::PartitionInfo {
1415            label: PART_2_NAME.to_string(),
1416            type_guid: crate::Guid::from_bytes(PART_TYPE_GUID),
1417            instance_guid: crate::Guid::from_bytes(PART_INSTANCE_2_GUID),
1418            start_block: 7,
1419            num_blocks: 1,
1420            flags: 0,
1421        });
1422        manager.commit_transaction(transaction).await.expect("Commit failed");
1423
1424        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1425        assert_eq!(manager.header().num_parts, 1);
1426        let partition = manager.partitions().get(&0).expect("No entry found");
1427        assert_eq!(partition.label, PART_1_NAME);
1428        assert_eq!(partition.type_guid.to_bytes(), PART_TYPE_GUID);
1429        assert_eq!(partition.instance_guid.to_bytes(), PART_INSTANCE_1_GUID);
1430        assert_eq!(partition.start_block, 4);
1431        assert_eq!(partition.num_blocks, 1);
1432        assert!(manager.partitions().get(&1).is_none());
1433    }
1434
1435    #[fuchsia::test]
1436    async fn restore_primary_from_backup() {
1437        const PART_TYPE_GUID: [u8; 16] = [2u8; 16];
1438        const PART_INSTANCE_GUID: [u8; 16] = [2u8; 16];
1439        const PART_NAME: &str = "part1";
1440
1441        let vmo = zx::Vmo::create(8192).unwrap();
1442        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1443        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1444
1445        let _task =
1446            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1447        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1448        Gpt::format(
1449            client.clone(),
1450            vec![PartitionInfo {
1451                label: PART_NAME.to_string(),
1452                type_guid: Guid::from_bytes(PART_TYPE_GUID),
1453                instance_guid: Guid::from_bytes(PART_INSTANCE_GUID),
1454                start_block: 4,
1455                num_blocks: 1,
1456                flags: 0,
1457            }],
1458        )
1459        .await
1460        .expect("format failed");
1461        let mut old_metadata = vec![0u8; 2048];
1462        client.read_at(MutableBufferSlice::Memory(&mut old_metadata[..]), 0).await.unwrap();
1463        let mut buffer = vec![0u8; 2048];
1464        client.write_at(BufferSlice::Memory(&buffer[..]), 0).await.unwrap();
1465
1466        let manager = Gpt::open(client).await.expect("load should succeed");
1467        let client = manager.take_client();
1468
1469        client.read_at(MutableBufferSlice::Memory(&mut buffer[..]), 0).await.unwrap();
1470        assert_eq!(old_metadata, buffer);
1471    }
1472
1473    #[fuchsia::test]
1474    async fn load_golden_gpt_linux() {
1475        let contents = std::fs::read("/pkg/data/gpt_golden/gpt.linux.blk").unwrap();
1476        let server = Arc::new(VmoBackedServer::new(contents.len() as u64 / 512, 512, &contents));
1477        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1478
1479        let _task =
1480            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1481        let manager = Gpt::open(Arc::new(RemoteBlockClient::new(client).await.unwrap()))
1482            .await
1483            .expect("load should succeed");
1484        let partition = manager.partitions().get(&0).expect("No entry found");
1485        assert_eq!(partition.label, "ext");
1486        assert_eq!(partition.type_guid.to_string(), "0fc63daf-8483-4772-8e79-3d69d8477de4");
1487        assert_eq!(partition.start_block, 8);
1488        assert_eq!(partition.num_blocks, 1);
1489        assert!(manager.partitions().get(&1).is_none());
1490    }
1491
1492    #[fuchsia::test]
1493    async fn load_golden_gpt_fuchsia() {
1494        let contents = std::fs::read("/pkg/data/gpt_golden/gpt.fuchsia.blk").unwrap();
1495        let server = Arc::new(VmoBackedServer::new(contents.len() as u64 / 512, 512, &contents));
1496        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1497
1498        struct ExpectedPartition {
1499            label: &'static str,
1500            type_guid: &'static str,
1501            blocks: Range<u64>,
1502        }
1503        const EXPECTED_PARTITIONS: [ExpectedPartition; 8] = [
1504            ExpectedPartition {
1505                label: "bootloader",
1506                type_guid: "5ece94fe-4c86-11e8-a15b-480fcf35f8e6",
1507                blocks: 11..12,
1508            },
1509            ExpectedPartition {
1510                label: "zircon_a",
1511                type_guid: "9b37fff6-2e58-466a-983a-f7926d0b04e0",
1512                blocks: 12..13,
1513            },
1514            ExpectedPartition {
1515                label: "zircon_b",
1516                type_guid: "9b37fff6-2e58-466a-983a-f7926d0b04e0",
1517                blocks: 13..14,
1518            },
1519            ExpectedPartition {
1520                label: "zircon_r",
1521                type_guid: "9b37fff6-2e58-466a-983a-f7926d0b04e0",
1522                blocks: 14..15,
1523            },
1524            ExpectedPartition {
1525                label: "vbmeta_a",
1526                type_guid: "421a8bfc-85d9-4d85-acda-b64eec0133e9",
1527                blocks: 15..16,
1528            },
1529            ExpectedPartition {
1530                label: "vbmeta_b",
1531                type_guid: "421a8bfc-85d9-4d85-acda-b64eec0133e9",
1532                blocks: 16..17,
1533            },
1534            ExpectedPartition {
1535                label: "vbmeta_r",
1536                type_guid: "421a8bfc-85d9-4d85-acda-b64eec0133e9",
1537                blocks: 17..18,
1538            },
1539            ExpectedPartition {
1540                label: "durable_boot",
1541                type_guid: "a409e16b-78aa-4acc-995c-302352621a41",
1542                blocks: 18..19,
1543            },
1544        ];
1545
1546        let _task =
1547            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1548        let manager = Gpt::open(Arc::new(RemoteBlockClient::new(client).await.unwrap()))
1549            .await
1550            .expect("load should succeed");
1551        for i in 0..EXPECTED_PARTITIONS.len() as u32 {
1552            let partition = manager.partitions().get(&i).expect("No entry found");
1553            let expected = &EXPECTED_PARTITIONS[i as usize];
1554            assert_eq!(partition.label, expected.label);
1555            assert_eq!(partition.type_guid.to_string(), expected.type_guid);
1556            assert_eq!(partition.start_block, expected.blocks.start);
1557            assert_eq!(partition.num_blocks, expected.blocks.end - expected.blocks.start);
1558        }
1559    }
1560
1561    #[fuchsia::test]
1562    async fn add_partitions_till_no_blocks_left() {
1563        let vmo = zx::Vmo::create(65536).unwrap();
1564        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1565        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1566
1567        let _task =
1568            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1569        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1570        Gpt::format(client.clone(), vec![PartitionInfo::nil(); 32]).await.expect("format failed");
1571        let mut manager = Gpt::open(client).await.expect("load should succeed");
1572        let mut transaction = manager.create_transaction().unwrap();
1573        assert_eq!(transaction.partitions.len(), 32);
1574        let mut num = 0;
1575        loop {
1576            match manager.add_partition(
1577                &mut transaction,
1578                crate::PartitionInfo {
1579                    label: format!("part-{num}"),
1580                    type_guid: crate::Guid::generate(),
1581                    instance_guid: crate::Guid::generate(),
1582                    start_block: 0,
1583                    num_blocks: 1,
1584                    flags: 0,
1585                },
1586            ) {
1587                Ok(_) => {
1588                    num += 1;
1589                }
1590                Err(AddPartitionError::InvalidArguments) => panic!("Unexpected error"),
1591                Err(AddPartitionError::NoSpace) => break,
1592            };
1593        }
1594        assert!(num <= 32);
1595        manager.commit_transaction(transaction).await.expect("Commit failed");
1596
1597        // Check state before and after a reload, to ensure both the in-memory and on-disk
1598        // representation match.
1599        assert_eq!(manager.header().num_parts, 32);
1600        assert_eq!(manager.partitions().len(), num);
1601
1602        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1603        assert_eq!(manager.header().num_parts, 32);
1604        assert_eq!(manager.partitions().len(), num);
1605    }
1606
1607    #[fuchsia::test]
1608    async fn add_partitions_till_no_slots_left() {
1609        let vmo = zx::Vmo::create(65536).unwrap();
1610        let server = Arc::new(VmoBackedServer::from_vmo(512, vmo));
1611        let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1612
1613        let _task =
1614            fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1615        let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1616        Gpt::format(client.clone(), vec![PartitionInfo::nil(); 4]).await.expect("format failed");
1617        let mut manager = Gpt::open(client).await.expect("load should succeed");
1618        let mut transaction = manager.create_transaction().unwrap();
1619        assert_eq!(transaction.partitions.len(), 4);
1620        let mut num = 0;
1621        loop {
1622            match manager.add_partition(
1623                &mut transaction,
1624                crate::PartitionInfo {
1625                    label: format!("part-{num}"),
1626                    type_guid: crate::Guid::generate(),
1627                    instance_guid: crate::Guid::generate(),
1628                    start_block: 0,
1629                    num_blocks: 1,
1630                    flags: 0,
1631                },
1632            ) {
1633                Ok(_) => {
1634                    num += 1;
1635                }
1636                Err(AddPartitionError::InvalidArguments) => panic!("Unexpected error"),
1637                Err(AddPartitionError::NoSpace) => break,
1638            };
1639        }
1640        assert!(num <= 4);
1641        manager.commit_transaction(transaction).await.expect("Commit failed");
1642
1643        // Check state before and after a reload, to ensure both the in-memory and on-disk
1644        // representation match.
1645        assert_eq!(manager.header().num_parts, 4);
1646        assert_eq!(manager.partitions().len(), num);
1647
1648        let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1649        assert_eq!(manager.header().num_parts, 4);
1650        assert_eq!(manager.partitions().len(), num);
1651    }
1652
1653    /// An Observer that shuffles writes and discards some of the tail since last flush.
1654    struct ShufflingObserver {
1655        // Only start shuffling once this is set.
1656        start: Arc<AtomicBool>,
1657        // Only shuffle if there is a write to this offset.
1658        shuffle_if_contains_offset: u64,
1659    }
1660
1661    impl vmo_backed_block_server::Observer for ShufflingObserver {
1662        fn flush(&self, writes: Option<&mut vmo_backed_block_server::Writes>) {
1663            if self.start.load(Ordering::Relaxed) {
1664                let Some(writes) = writes else { unreachable!() };
1665                if writes
1666                    .iter()
1667                    .filter(|(offset, _)| **offset == self.shuffle_if_contains_offset)
1668                    .next()
1669                    .is_some()
1670                {
1671                    writes.shuffle();
1672                    writes.discard_some();
1673                }
1674            }
1675        }
1676
1677        fn close(&self, writes: Option<&mut vmo_backed_block_server::Writes>) {
1678            // Always shuffle every write which had yet to be flushed when the client closed.
1679            if self.start.load(Ordering::Relaxed) {
1680                let Some(writes) = writes else { unreachable!() };
1681                writes.shuffle();
1682            }
1683        }
1684    }
1685
1686    #[fuchsia::test]
1687    async fn metadata_update_is_atomic() {
1688        const BLOCK_SIZE: u64 = 512;
1689        const BLOCK_COUNT: u64 = 128;
1690        // Test once where we shuffle any set of writes which contains the primary superblock, and
1691        // once where we shuffle any set of writes which contains the secondary superblock.
1692        // The goal is to ensure that writes are correctly sequenced with some sort of flush or
1693        // barrier (secondary, <barrier>, primary), so metadata updates are atomic.
1694        for shuffle_if_contains_offset in [1, BLOCK_COUNT - 1] {
1695            let vmo = zx::Vmo::create(BLOCK_SIZE * BLOCK_COUNT).unwrap();
1696            let start_shuffling = Arc::new(AtomicBool::new(false));
1697            let server = Arc::new(
1698                VmoBackedServerOptions {
1699                    initial_contents: InitialContents::FromVmo(vmo),
1700                    block_size: BLOCK_SIZE as u32,
1701                    observer: Some(Box::new(ShufflingObserver {
1702                        start: start_shuffling.clone(),
1703                        shuffle_if_contains_offset,
1704                    })),
1705                    write_tracking: true,
1706                    ..Default::default()
1707                }
1708                .build()
1709                .unwrap(),
1710            );
1711            let (client, server_end) = fidl::endpoints::create_proxy::<fvolume::VolumeMarker>();
1712            let _task =
1713                fasync::Task::spawn(async move { server.serve(server_end.into_stream()).await });
1714            let client = Arc::new(RemoteBlockClient::new(client).await.unwrap());
1715            Gpt::format(client.clone(), vec![PartitionInfo::nil(); 80])
1716                .await
1717                .expect("format failed");
1718
1719            start_shuffling.store(true, Ordering::Relaxed);
1720
1721            let mut manager = Gpt::open(client).await.expect("load should succeed");
1722            let mut transaction = manager.create_transaction().unwrap();
1723            transaction.partitions.truncate(40);
1724            let mut num = 0;
1725            loop {
1726                match manager.add_partition(
1727                    &mut transaction,
1728                    crate::PartitionInfo {
1729                        label: format!("part-{num}"),
1730                        type_guid: crate::Guid::generate(),
1731                        instance_guid: crate::Guid::generate(),
1732                        start_block: 0,
1733                        num_blocks: 1,
1734                        flags: 0,
1735                    },
1736                ) {
1737                    Ok(_) => {
1738                        num += 1;
1739                    }
1740                    Err(AddPartitionError::InvalidArguments) => panic!("Unexpected error"),
1741                    Err(AddPartitionError::NoSpace) => break,
1742                };
1743            }
1744            assert!(num <= 40);
1745            manager.commit_transaction(transaction).await.expect("Commit failed");
1746
1747            // Check state before and after a reload.
1748            assert_eq!(manager.header().num_parts, 40);
1749            assert_eq!(manager.partitions().len(), num);
1750
1751            // If the GPT implementation has appropriate barriers/flushes between secondary and
1752            // primary metadata updates, then we will end up in either the old state or the new
1753            // state.  Otherwise, both copies might become corrupt and the GPT would be unreadable.
1754            let manager = Gpt::open(manager.take_client()).await.expect("reload should succeed");
1755            let len = manager.partitions().len();
1756            assert!(len == 0 || len == num);
1757        }
1758    }
1759}