Skip to main content

bedrock_world/
storage.rs

1//! Storage abstraction used by [`crate::BedrockWorld`].
2//!
3//! The trait in this module keeps world parsing independent from a concrete
4//! LevelDB implementation. [`MemoryStorage`] is useful for tests and synthetic
5//! tools, while [`BedrockLevelDbStorage`](crate::BedrockLevelDbStorage) adapts
6//! the `bedrock-leveldb` crate.
7
8use crate::chunk::{ChunkKey, ChunkPos, ChunkRecordTag, Dimension, LEGACY_TERRAIN_VALUE_LEN};
9use crate::error::{BedrockWorldError, Result};
10use crate::level_dat::read_level_dat_document;
11use crate::nbt::NbtTag;
12use bytes::Bytes;
13use std::collections::BTreeMap;
14use std::fs;
15use std::path::Path;
16use std::sync::{
17    Arc, RwLock,
18    atomic::{AtomicBool, Ordering},
19};
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22/// Owned raw key/value storage entry.
23pub struct StorageEntry {
24    /// Decoded storage key for this record.
25    pub key: Bytes,
26    /// Parsed or raw value associated with this record.
27    pub value: Bytes,
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31/// Borrowed raw key/value storage entry view.
32pub struct StorageEntryRef<'a> {
33    /// Decoded storage key for this record.
34    pub key: &'a [u8],
35    /// Parsed or raw value associated with this record.
36    pub value: &'a [u8],
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40/// Raw storage mutation operation.
41pub enum StorageOp {
42    /// Writes or replaces a raw storage value.
43    Put {
44        /// Decoded storage key for this record.
45        key: Bytes,
46        /// Raw value bytes written for the key.
47        value: Bytes,
48    },
49    /// Delete operation.
50    Delete {
51        /// Decoded storage key for this record.
52        key: Bytes,
53    },
54}
55
56#[derive(Debug, Clone)]
57/// Options controlling raw storage reads and scans.
58pub struct StorageReadOptions {
59    /// Threading policy for this operation.
60    pub threading: StorageThreadingOptions,
61    /// Scan strategy requested from the backend.
62    pub scan_mode: StorageScanMode,
63    /// Backend cache strategy for table/data blocks.
64    pub cache_policy: StorageCachePolicy,
65    /// Bounded pipeline settings for this operation.
66    pub pipeline: StoragePipelineOptions,
67    /// Optional cancellation flag checked during long-running work.
68    pub cancel: Option<StorageCancelFlag>,
69    /// Optional progress sink invoked during long-running work.
70    pub progress: Option<StorageProgressSink>,
71}
72
73impl Default for StorageReadOptions {
74    fn default() -> Self {
75        Self {
76            threading: StorageThreadingOptions::Auto,
77            scan_mode: StorageScanMode::ParallelTables,
78            cache_policy: StorageCachePolicy::Bypass,
79            pipeline: StoragePipelineOptions::default(),
80            cancel: None,
81            progress: None,
82        }
83    }
84}
85
86#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
87/// Cache policy for backend storage reads.
88pub enum StorageCachePolicy {
89    #[default]
90    /// Bypass shared backend block caches.
91    Bypass,
92    /// Use shared backend block caches when available.
93    Use,
94}
95
96#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
97/// Bounded pipeline settings for storage scans.
98pub struct StoragePipelineOptions {
99    /// Maximum queued work items; zero selects an automatic default.
100    pub queue_depth: usize,
101    /// Table batch size; zero selects an automatic default.
102    pub table_batch_size: usize,
103    /// Progress callback interval; zero selects an automatic default.
104    pub progress_interval: usize,
105}
106
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
108/// Threading policy for storage operations.
109pub enum StorageThreadingOptions {
110    #[default]
111    /// Automatically choose the appropriate mode.
112    Auto,
113    /// Use a fixed worker count.
114    Fixed(usize),
115    /// Use a single worker.
116    Single,
117}
118
119#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
120/// Scan strategy requested from a storage backend.
121pub enum StorageScanMode {
122    #[default]
123    /// Scan sequentially.
124    Sequential,
125    /// Scan backend tables in parallel when supported.
126    ParallelTables,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130/// Visitor control flow for storage scans.
131pub enum StorageVisitorControl {
132    /// Continue visiting records.
133    Continue,
134    /// Stop visiting records.
135    Stop,
136}
137
138#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
139/// Diagnostics collected by a storage scan.
140pub struct StorageScanOutcome {
141    /// Number of entries visited by the scan.
142    pub visited: usize,
143    /// Number of value bytes read while scanning.
144    pub bytes_read: usize,
145    /// Whether a visitor requested early termination.
146    pub stopped: bool,
147    /// Number of backend tables scanned.
148    pub tables_scanned: usize,
149    /// Number of worker threads used by the operation.
150    pub worker_threads: usize,
151    /// Milliseconds spent waiting for bounded pipeline capacity.
152    pub queue_wait_ms: u128,
153    /// Number of cancellation checks performed.
154    pub cancel_checks: usize,
155}
156
157impl StorageScanOutcome {
158    #[must_use]
159    /// Returns an empty scan outcome with all counters set to zero.
160    pub const fn empty() -> Self {
161        Self {
162            visited: 0,
163            bytes_read: 0,
164            stopped: false,
165            tables_scanned: 0,
166            worker_threads: 0,
167            queue_wait_ms: 0,
168            cancel_checks: 0,
169        }
170    }
171
172    /// Records one visited value and its byte length.
173    pub fn record(&mut self, value_len: usize) {
174        self.visited = self.visited.saturating_add(1);
175        self.bytes_read = self.bytes_read.saturating_add(value_len);
176    }
177
178    /// Merges another scan outcome into this one.
179    pub fn merge(&mut self, other: Self) {
180        self.visited = self.visited.saturating_add(other.visited);
181        self.bytes_read = self.bytes_read.saturating_add(other.bytes_read);
182        self.stopped |= other.stopped;
183        self.tables_scanned = self.tables_scanned.saturating_add(other.tables_scanned);
184        self.worker_threads = self.worker_threads.max(other.worker_threads);
185        self.queue_wait_ms = self.queue_wait_ms.saturating_add(other.queue_wait_ms);
186        self.cancel_checks = self.cancel_checks.saturating_add(other.cancel_checks);
187    }
188}
189
190#[derive(Debug, Clone, Default)]
191/// Shareable cancellation flag for storage operations.
192pub struct StorageCancelFlag(Arc<AtomicBool>);
193
194impl StorageCancelFlag {
195    #[must_use]
196    /// Creates a new value.
197    pub fn new() -> Self {
198        Self::default()
199    }
200
201    /// Requests cancellation for operations sharing this flag.
202    pub fn cancel(&self) {
203        self.0.store(true, Ordering::Relaxed);
204    }
205
206    #[must_use]
207    /// Creates a cancellation flag from a shared atomic boolean.
208    pub fn from_shared(cancelled: Arc<AtomicBool>) -> Self {
209        Self(cancelled)
210    }
211
212    #[must_use]
213    /// Returns whether cancellation has been requested.
214    pub fn is_cancelled(&self) -> bool {
215        self.0.load(Ordering::Relaxed)
216    }
217}
218
219#[derive(Clone)]
220/// Callback sink for storage progress updates.
221pub struct StorageProgressSink {
222    inner: Arc<dyn Fn(StorageScanProgress) + Send + Sync>,
223}
224
225impl std::fmt::Debug for StorageProgressSink {
226    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        formatter
228            .debug_struct("StorageProgressSink")
229            .finish_non_exhaustive()
230    }
231}
232
233impl StorageProgressSink {
234    #[must_use]
235    /// Creates a new value.
236    pub fn new(callback: impl Fn(StorageScanProgress) + Send + Sync + 'static) -> Self {
237        Self {
238            inner: Arc::new(callback),
239        }
240    }
241
242    /// Emits a progress update to the callback.
243    pub fn emit(&self, progress: StorageScanProgress) {
244        (self.inner)(progress);
245    }
246}
247
248#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
249/// Progress update emitted by a storage backend.
250pub struct StorageScanProgress {
251    /// Number of entries observed when progress was emitted.
252    pub entries_seen: usize,
253    /// Number of value bytes read while scanning.
254    pub bytes_read: usize,
255}
256
257#[derive(Debug, Clone, Default, PartialEq, Eq)]
258/// Buffered batch of raw storage operations.
259pub struct StorageBatch {
260    ops: Vec<StorageOp>,
261}
262
263impl StorageBatch {
264    #[must_use]
265    /// Creates a new value.
266    pub const fn new() -> Self {
267        Self { ops: Vec::new() }
268    }
269
270    /// Adds a raw put operation to this batch.
271    pub fn put(&mut self, key: impl Into<Bytes>, value: impl Into<Bytes>) {
272        self.ops.push(StorageOp::Put {
273            key: key.into(),
274            value: value.into(),
275        });
276    }
277
278    /// Adds a raw delete operation to this batch.
279    pub fn delete(&mut self, key: impl Into<Bytes>) {
280        self.ops.push(StorageOp::Delete { key: key.into() });
281    }
282
283    #[must_use]
284    /// Returns whether this batch contains no operations.
285    pub fn is_empty(&self) -> bool {
286        self.ops.is_empty()
287    }
288
289    #[must_use]
290    /// Returns the buffered operations.
291    pub fn ops(&self) -> &[StorageOp] {
292        &self.ops
293    }
294}
295
296/// Raw key/value storage abstraction used by [`BedrockWorld`](crate::BedrockWorld).
297pub trait WorldStorage: Send + Sync {
298    /// Looks up a raw value by exact key.
299    fn get(&self, key: &[u8]) -> Result<Option<Bytes>>;
300    /// Looks up raw values by exact key, preserving input order.
301    fn get_many(&self, keys: &[Bytes]) -> Result<Vec<Option<Bytes>>> {
302        keys.iter().map(|key| self.get(key)).collect()
303    }
304    /// Looks up raw values by exact key with read options and cancellation, preserving input order.
305    fn get_many_ordered_with_control(
306        &self,
307        keys: &[Bytes],
308        options: StorageReadOptions,
309    ) -> Result<Vec<Option<Bytes>>> {
310        check_cancelled(&options)?;
311        let mut values = Vec::with_capacity(keys.len());
312        for key in keys {
313            check_cancelled(&options)?;
314            values.push(self.get(key)?);
315        }
316        Ok(values)
317    }
318    /// Writes a raw key/value pair.
319    fn put(&self, key: &[u8], value: &[u8]) -> Result<()>;
320    /// Deletes a raw key.
321    fn delete(&self, key: &[u8]) -> Result<()>;
322    /// Visits keys without forcing value materialization when the backend can
323    /// support key-only scans.
324    fn for_each_key(
325        &self,
326        options: StorageReadOptions,
327        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
328    ) -> Result<StorageScanOutcome>;
329    /// Visits key/value records whose key starts with `prefix`.
330    fn for_each_prefix(
331        &self,
332        prefix: &[u8],
333        options: StorageReadOptions,
334        visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
335    ) -> Result<StorageScanOutcome>;
336    /// Visits key/value records as borrowed byte views.
337    fn for_each_prefix_ref(
338        &self,
339        prefix: &[u8],
340        options: StorageReadOptions,
341        visitor: &mut (dyn FnMut(StorageEntryRef<'_>) -> Result<StorageVisitorControl> + Send),
342    ) -> Result<StorageScanOutcome> {
343        self.for_each_prefix(prefix, options, &mut |key, value| {
344            visitor(StorageEntryRef {
345                key,
346                value: value.as_ref(),
347            })
348        })
349    }
350    /// Visits keys whose key starts with `prefix` without requiring value
351    /// materialization when the backend can support key-only scans.
352    fn for_each_prefix_key(
353        &self,
354        prefix: &[u8],
355        options: StorageReadOptions,
356        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
357    ) -> Result<StorageScanOutcome> {
358        self.for_each_prefix(prefix, options, &mut |key, _value| visitor(key))
359    }
360    /// Visits all key/value records.
361    fn for_each_entry(
362        &self,
363        options: StorageReadOptions,
364        visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
365    ) -> Result<StorageScanOutcome> {
366        self.for_each_prefix(b"", options, visitor)
367    }
368    /// Applies a batch of raw operations atomically when supported by the backend.
369    fn write_batch(&self, batch: &StorageBatch) -> Result<()>;
370    /// Flushes pending writes to durable storage when supported by the backend.
371    fn flush(&self) -> Result<()>;
372    /// Rewrites visible storage data to remove obsolete delete markers and old tables.
373    fn compact(&self) -> Result<()> {
374        self.flush()
375    }
376}
377
378#[derive(Debug, Clone, Default)]
379/// In-memory storage backend for tests and synthetic tools.
380pub struct MemoryStorage {
381    values: Arc<RwLock<BTreeMap<Vec<u8>, Bytes>>>,
382}
383
384impl MemoryStorage {
385    #[must_use]
386    /// Creates a new value.
387    pub fn new() -> Self {
388        Self::default()
389    }
390}
391
392impl WorldStorage for MemoryStorage {
393    fn get(&self, key: &[u8]) -> Result<Option<Bytes>> {
394        let values = self.values.read().map_err(|_| {
395            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
396        })?;
397        Ok(values.get(key).cloned())
398    }
399
400    fn get_many(&self, keys: &[Bytes]) -> Result<Vec<Option<Bytes>>> {
401        let values = self.values.read().map_err(|_| {
402            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
403        })?;
404        Ok(keys
405            .iter()
406            .map(|key| values.get(key.as_ref()).cloned())
407            .collect())
408    }
409
410    fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
411        let mut values = self.values.write().map_err(|_| {
412            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
413        })?;
414        values.insert(key.to_vec(), Bytes::copy_from_slice(value));
415        Ok(())
416    }
417
418    fn delete(&self, key: &[u8]) -> Result<()> {
419        let mut values = self.values.write().map_err(|_| {
420            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
421        })?;
422        values.remove(key);
423        Ok(())
424    }
425
426    fn for_each_key(
427        &self,
428        options: StorageReadOptions,
429        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
430    ) -> Result<StorageScanOutcome> {
431        let values = self.values.read().map_err(|_| {
432            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
433        })?;
434        let mut outcome = StorageScanOutcome::empty();
435        for (key, value) in values.iter() {
436            check_cancelled(&options)?;
437            outcome.record(value.len());
438            if visitor(key)? == StorageVisitorControl::Stop {
439                outcome.stopped = true;
440                return Ok(outcome);
441            }
442            emit_progress(&options, outcome);
443        }
444        Ok(outcome)
445    }
446
447    fn for_each_prefix(
448        &self,
449        prefix: &[u8],
450        options: StorageReadOptions,
451        visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
452    ) -> Result<StorageScanOutcome> {
453        let values = self.values.read().map_err(|_| {
454            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
455        })?;
456        let mut outcome = StorageScanOutcome::empty();
457        for (key, value) in values
458            .range(prefix.to_vec()..)
459            .take_while(|(key, _)| key.starts_with(prefix))
460        {
461            check_cancelled(&options)?;
462            outcome.record(value.len());
463            if visitor(key, value)? == StorageVisitorControl::Stop {
464                outcome.stopped = true;
465                return Ok(outcome);
466            }
467            emit_progress(&options, outcome);
468        }
469        Ok(outcome)
470    }
471
472    fn for_each_prefix_key(
473        &self,
474        prefix: &[u8],
475        options: StorageReadOptions,
476        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
477    ) -> Result<StorageScanOutcome> {
478        let values = self.values.read().map_err(|_| {
479            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
480        })?;
481        let mut outcome = StorageScanOutcome::empty();
482        for (key, value) in values
483            .range(prefix.to_vec()..)
484            .take_while(|(key, _)| key.starts_with(prefix))
485        {
486            check_cancelled(&options)?;
487            outcome.record(value.len());
488            if visitor(key)? == StorageVisitorControl::Stop {
489                outcome.stopped = true;
490                return Ok(outcome);
491            }
492            emit_progress(&options, outcome);
493        }
494        Ok(outcome)
495    }
496
497    fn write_batch(&self, batch: &StorageBatch) -> Result<()> {
498        let mut values = self.values.write().map_err(|_| {
499            BedrockWorldError::ConcurrentWrite("memory storage poisoned".to_string())
500        })?;
501        for op in batch.ops() {
502            match op {
503                StorageOp::Put { key, value } => {
504                    values.insert(key.to_vec(), value.clone());
505                }
506                StorageOp::Delete { key } => {
507                    values.remove(key.as_ref());
508                }
509            }
510        }
511        Ok(())
512    }
513
514    fn flush(&self) -> Result<()> {
515        Ok(())
516    }
517
518    fn compact(&self) -> Result<()> {
519        Ok(())
520    }
521}
522
523/// Terrain payload length used by old Pocket Edition `chunks.dat` files before
524/// the 1024-byte `[biome_id, red, green, blue]` tail was added to `LevelDB`
525/// `LegacyTerrain`.
526pub const POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN: usize = 82_176;
527const POCKET_CHUNKS_DAT_LOCATION_TABLE_LEN: usize = 4 * 32 * 32;
528const POCKET_CHUNKS_DAT_SECTOR_BYTES: usize = 4096;
529const DEFAULT_LEGACY_BIOME_SAMPLE: [u8; 4] = [1, 0x7f, 0xb2, 0x38];
530
531#[derive(Debug, Clone)]
532/// Read-only backend for pre-LevelDB Pocket Edition chunks.dat worlds.
533pub struct PocketChunksDatStorage {
534    values: Arc<BTreeMap<Vec<u8>, Bytes>>,
535    origin_chunk_x: i32,
536    origin_chunk_z: i32,
537}
538
539impl PocketChunksDatStorage {
540    /// Opens a read-only backend for a pre-`LevelDB` Pocket Edition world folder.
541    pub fn open(world_path: impl AsRef<Path>) -> Result<Self> {
542        let world_path = world_path.as_ref();
543        let chunks_path = world_path.join("chunks.dat");
544        let bytes = fs::read(&chunks_path)?;
545        let (origin_chunk_x, origin_chunk_z) = read_limited_world_origin(world_path);
546        let values = parse_pocket_chunks_dat(&bytes, origin_chunk_x, origin_chunk_z)?;
547        if world_path.join("entities.dat").is_file() {
548            match fs::read(world_path.join("entities.dat")) {
549                Ok(bytes) => log::debug!(
550                    "legacy entities.dat present (bytes={}, parser=best_effort_skip)",
551                    bytes.len()
552                ),
553                Err(error) => log::warn!("failed to read legacy entities.dat: {error}"),
554            }
555        }
556        log::debug!(
557            "opened Pocket chunks.dat storage (chunks={}, origin=({}, {}), path={})",
558            values.len(),
559            origin_chunk_x,
560            origin_chunk_z,
561            chunks_path.display()
562        );
563        Ok(Self {
564            values: Arc::new(values),
565            origin_chunk_x,
566            origin_chunk_z,
567        })
568    }
569
570    #[must_use]
571    /// Origin chunk x.
572    pub const fn origin_chunk_x(&self) -> i32 {
573        self.origin_chunk_x
574    }
575
576    #[must_use]
577    /// Origin chunk z.
578    pub const fn origin_chunk_z(&self) -> i32 {
579        self.origin_chunk_z
580    }
581}
582
583impl WorldStorage for PocketChunksDatStorage {
584    fn get(&self, key: &[u8]) -> Result<Option<Bytes>> {
585        Ok(self.values.get(key).cloned())
586    }
587
588    fn get_many(&self, keys: &[Bytes]) -> Result<Vec<Option<Bytes>>> {
589        Ok(keys
590            .iter()
591            .map(|key| self.values.get(key.as_ref()).cloned())
592            .collect())
593    }
594
595    fn put(&self, _key: &[u8], _value: &[u8]) -> Result<()> {
596        Err(pocket_chunks_dat_read_only_error())
597    }
598
599    fn delete(&self, _key: &[u8]) -> Result<()> {
600        Err(pocket_chunks_dat_read_only_error())
601    }
602
603    fn for_each_key(
604        &self,
605        options: StorageReadOptions,
606        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
607    ) -> Result<StorageScanOutcome> {
608        let mut outcome = StorageScanOutcome::empty();
609        for (key, value) in self.values.iter() {
610            check_cancelled(&options)?;
611            outcome.record(value.len());
612            if visitor(key)? == StorageVisitorControl::Stop {
613                outcome.stopped = true;
614                return Ok(outcome);
615            }
616            emit_progress(&options, outcome);
617        }
618        Ok(outcome)
619    }
620
621    fn for_each_prefix(
622        &self,
623        prefix: &[u8],
624        options: StorageReadOptions,
625        visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
626    ) -> Result<StorageScanOutcome> {
627        let mut outcome = StorageScanOutcome::empty();
628        for (key, value) in self
629            .values
630            .range(prefix.to_vec()..)
631            .take_while(|(key, _)| key.starts_with(prefix))
632        {
633            check_cancelled(&options)?;
634            outcome.record(value.len());
635            if visitor(key, value)? == StorageVisitorControl::Stop {
636                outcome.stopped = true;
637                return Ok(outcome);
638            }
639            emit_progress(&options, outcome);
640        }
641        Ok(outcome)
642    }
643
644    fn for_each_prefix_key(
645        &self,
646        prefix: &[u8],
647        options: StorageReadOptions,
648        visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
649    ) -> Result<StorageScanOutcome> {
650        let mut outcome = StorageScanOutcome::empty();
651        for (key, value) in self
652            .values
653            .range(prefix.to_vec()..)
654            .take_while(|(key, _)| key.starts_with(prefix))
655        {
656            check_cancelled(&options)?;
657            outcome.record(value.len());
658            if visitor(key)? == StorageVisitorControl::Stop {
659                outcome.stopped = true;
660                return Ok(outcome);
661            }
662            emit_progress(&options, outcome);
663        }
664        Ok(outcome)
665    }
666
667    fn write_batch(&self, _batch: &StorageBatch) -> Result<()> {
668        Err(pocket_chunks_dat_read_only_error())
669    }
670
671    fn flush(&self) -> Result<()> {
672        Ok(())
673    }
674
675    fn compact(&self) -> Result<()> {
676        Ok(())
677    }
678}
679
680fn parse_pocket_chunks_dat(
681    bytes: &[u8],
682    origin_chunk_x: i32,
683    origin_chunk_z: i32,
684) -> Result<BTreeMap<Vec<u8>, Bytes>> {
685    if bytes.len() < POCKET_CHUNKS_DAT_LOCATION_TABLE_LEN {
686        return Err(BedrockWorldError::CorruptWorld(format!(
687            "chunks.dat is too small for its location table: {} bytes",
688            bytes.len()
689        )));
690    }
691    let mut values = BTreeMap::new();
692    for index in 0..(32 * 32) {
693        let entry_offset = index * 4;
694        let entry = &bytes[entry_offset..entry_offset + 4];
695        if entry == [0, 0, 0, 0] {
696            continue;
697        }
698        let sector_count = usize::from(entry[0]);
699        let sector_offset =
700            usize::from(entry[1]) | (usize::from(entry[2]) << 8) | (usize::from(entry[3]) << 16);
701        if sector_count == 0 || sector_offset == 0 {
702            continue;
703        }
704        let Some(byte_offset) = sector_offset.checked_mul(POCKET_CHUNKS_DAT_SECTOR_BYTES) else {
705            continue;
706        };
707        let Some(payload) = pocket_chunk_payload(bytes, byte_offset, sector_count) else {
708            log::warn!(
709                "skipping invalid chunks.dat entry (index={index}, sector_offset={sector_offset}, sector_count={sector_count})"
710            );
711            continue;
712        };
713        let local_x = i32::try_from(index % 32).unwrap_or(0);
714        let local_z = i32::try_from(index / 32).unwrap_or(0);
715        let pos = ChunkPos {
716            x: origin_chunk_x.saturating_add(local_x),
717            z: origin_chunk_z.saturating_add(local_z),
718            dimension: Dimension::Overworld,
719        };
720        values.insert(
721            ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain)
722                .encode()
723                .to_vec(),
724            convert_pocket_terrain_to_legacy(payload),
725        );
726    }
727    Ok(values)
728}
729
730fn pocket_chunk_payload(bytes: &[u8], byte_offset: usize, sector_count: usize) -> Option<&[u8]> {
731    let sector_bytes = sector_count.checked_mul(POCKET_CHUNKS_DAT_SECTOR_BYTES)?;
732    let max_end = byte_offset.checked_add(sector_bytes)?.min(bytes.len());
733    if byte_offset >= bytes.len() || byte_offset >= max_end {
734        return None;
735    }
736    let available = &bytes[byte_offset..max_end];
737    if available.len() >= 4 {
738        let declared_len = u32::from_le_bytes(available[0..4].try_into().ok()?) as usize;
739        if declared_len == POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN
740            && available.len() >= 4 + declared_len
741        {
742            return Some(&available[4..4 + declared_len]);
743        }
744        if declared_len == LEGACY_TERRAIN_VALUE_LEN && available.len() >= 4 + declared_len {
745            return Some(&available[4..4 + POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN]);
746        }
747    }
748    if available.len() >= POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN {
749        return Some(&available[..POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN]);
750    }
751    None
752}
753
754fn convert_pocket_terrain_to_legacy(payload: &[u8]) -> Bytes {
755    if payload.len() == LEGACY_TERRAIN_VALUE_LEN {
756        return Bytes::copy_from_slice(payload);
757    }
758    let mut legacy = Vec::with_capacity(LEGACY_TERRAIN_VALUE_LEN);
759    legacy.extend_from_slice(&payload[..POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN]);
760    for _ in 0..256 {
761        legacy.extend_from_slice(&DEFAULT_LEGACY_BIOME_SAMPLE);
762    }
763    Bytes::from(legacy)
764}
765
766fn read_limited_world_origin(world_path: &Path) -> (i32, i32) {
767    let Ok(document) = read_level_dat_document(&world_path.join("level.dat")) else {
768        return (0, 0);
769    };
770    let NbtTag::Compound(root) = document.root else {
771        return (0, 0);
772    };
773    (
774        nbt_i32(root.get("LimitedWorldOriginX")).unwrap_or(0),
775        nbt_i32(root.get("LimitedWorldOriginZ")).unwrap_or(0),
776    )
777}
778
779fn nbt_i32(tag: Option<&NbtTag>) -> Option<i32> {
780    match tag {
781        Some(NbtTag::Byte(value)) => Some(i32::from(*value)),
782        Some(NbtTag::Short(value)) => Some(i32::from(*value)),
783        Some(NbtTag::Int(value)) => Some(*value),
784        Some(NbtTag::Long(value)) => i32::try_from(*value).ok(),
785        _ => None,
786    }
787}
788
789fn pocket_chunks_dat_read_only_error() -> BedrockWorldError {
790    BedrockWorldError::UnsupportedChunkFormat("Pocket chunks.dat storage is read-only".to_string())
791}
792
793/// Backend module.
794pub mod backend {
795    use super::*;
796
797    #[cfg(feature = "backend-bedrock-leveldb")]
798    #[derive(Clone)]
799    /// Bedrock level db storage data model.
800    pub struct BedrockLevelDbStorage {
801        db: Arc<bedrock_leveldb::Db>,
802    }
803
804    #[cfg(feature = "backend-bedrock-leveldb")]
805    impl BedrockLevelDbStorage {
806        /// Opens a `LevelDB` directory for read/write access.
807        pub fn open(path: impl AsRef<Path>) -> Result<Self> {
808            Self::open_inner(path, false, true)
809        }
810
811        /// Opens a `LevelDB` directory without allowing backend writes.
812        pub fn open_read_only(path: impl AsRef<Path>) -> Result<Self> {
813            Self::open_inner(path, true, true)
814        }
815
816        /// Opens a read-only database while allowing table blocks with invalid checksums.
817        ///
818        /// This is intended for best-effort inspection of damaged worlds. The returned
819        /// storage never writes to the database.
820        pub fn open_read_only_best_effort(path: impl AsRef<Path>) -> Result<Self> {
821            Self::open_inner(path, true, false)
822        }
823
824        fn open_inner(
825            path: impl AsRef<Path>,
826            read_only: bool,
827            paranoid_checks: bool,
828        ) -> Result<Self> {
829            let path = path.as_ref().to_path_buf();
830            if !path.exists() {
831                return Err(BedrockWorldError::Io(std::io::Error::new(
832                    std::io::ErrorKind::NotFound,
833                    format!("LevelDB path not found: {}", path.display()),
834                )));
835            }
836            let options = bedrock_leveldb::OpenOptions {
837                read_only,
838                create_if_missing: false,
839                error_if_exists: false,
840                paranoid_checks,
841                compression_policy: bedrock_leveldb::CompressionPolicy::Zlib,
842                cache_size: 64 * 1024 * 1024,
843                write_buffer_size: 0,
844            };
845            let db = bedrock_leveldb::Db::open(path, options).map_err(map_leveldb_error)?;
846            Ok(Self { db: Arc::new(db) })
847        }
848    }
849
850    #[cfg(feature = "backend-bedrock-leveldb")]
851    impl WorldStorage for BedrockLevelDbStorage {
852        fn get(&self, key: &[u8]) -> Result<Option<Bytes>> {
853            self.db.get(key).map_err(map_leveldb_error)
854        }
855
856        fn get_many(&self, keys: &[Bytes]) -> Result<Vec<Option<Bytes>>> {
857            self.db
858                .get_many_owned(
859                    keys.iter().cloned(),
860                    bedrock_leveldb::ReadOptions::default(),
861                )
862                .map_err(map_leveldb_error)
863        }
864
865        fn get_many_ordered_with_control(
866            &self,
867            keys: &[Bytes],
868            options: StorageReadOptions,
869        ) -> Result<Vec<Option<Bytes>>> {
870            check_cancelled(&options)?;
871            self.db
872                .get_many_owned(keys.iter().cloned(), to_leveldb_read_options(options))
873                .map_err(map_leveldb_error)
874        }
875
876        fn put(&self, key: &[u8], value: &[u8]) -> Result<()> {
877            self.db
878                .put(
879                    Bytes::copy_from_slice(key),
880                    Bytes::copy_from_slice(value),
881                    write_options(),
882                )
883                .map_err(map_leveldb_error)
884        }
885
886        fn delete(&self, key: &[u8]) -> Result<()> {
887            self.db
888                .delete(Bytes::copy_from_slice(key), write_options())
889                .map_err(map_leveldb_error)
890        }
891
892        fn for_each_key(
893            &self,
894            options: StorageReadOptions,
895            visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
896        ) -> Result<StorageScanOutcome> {
897            let read_options = to_leveldb_read_options(options);
898            let mut visitor_error = None;
899            let scan_result = self
900                .db
901                .for_each_key(read_options, |key| match visitor(key) {
902                    Ok(StorageVisitorControl::Continue) => {
903                        Ok(bedrock_leveldb::VisitorControl::Continue)
904                    }
905                    Ok(StorageVisitorControl::Stop) => Ok(bedrock_leveldb::VisitorControl::Stop),
906                    Err(error) => {
907                        visitor_error = Some(error);
908                        Ok(bedrock_leveldb::VisitorControl::Stop)
909                    }
910                });
911            match (scan_result, visitor_error) {
912                (_, Some(error)) => Err(error),
913                (Ok(outcome), None) => Ok(to_storage_outcome(outcome)),
914                (Err(error), None) => Err(map_leveldb_error(error)),
915            }
916        }
917
918        fn for_each_prefix(
919            &self,
920            prefix: &[u8],
921            options: StorageReadOptions,
922            visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
923        ) -> Result<StorageScanOutcome> {
924            let read_options = to_leveldb_read_options(options);
925            let mut visitor_error = None;
926            let scan_result = self.db.for_each_prefix(prefix, read_options, |key, value| {
927                match visitor(key, value) {
928                    Ok(StorageVisitorControl::Continue) => {
929                        Ok(bedrock_leveldb::VisitorControl::Continue)
930                    }
931                    Ok(StorageVisitorControl::Stop) => Ok(bedrock_leveldb::VisitorControl::Stop),
932                    Err(error) => {
933                        visitor_error = Some(error);
934                        Ok(bedrock_leveldb::VisitorControl::Stop)
935                    }
936                }
937            });
938            match (scan_result, visitor_error) {
939                (_, Some(error)) => Err(error),
940                (Ok(outcome), None) => Ok(to_storage_outcome(outcome)),
941                (Err(error), None) => Err(map_leveldb_error(error)),
942            }
943        }
944
945        fn for_each_prefix_ref(
946            &self,
947            prefix: &[u8],
948            options: StorageReadOptions,
949            visitor: &mut (dyn FnMut(StorageEntryRef<'_>) -> Result<StorageVisitorControl> + Send),
950        ) -> Result<StorageScanOutcome> {
951            let mut read_options = to_leveldb_read_options(options);
952            read_options.read_strategy = bedrock_leveldb::ReadStrategy::Borrowed;
953            let mut visitor_error = None;
954            let scan_result = self.db.for_each_prefix_ref(prefix, read_options, |entry| {
955                match visitor(StorageEntryRef {
956                    key: entry.key.as_bytes(),
957                    value: entry.value.as_bytes(),
958                }) {
959                    Ok(StorageVisitorControl::Continue) => {
960                        Ok(bedrock_leveldb::VisitorControl::Continue)
961                    }
962                    Ok(StorageVisitorControl::Stop) => Ok(bedrock_leveldb::VisitorControl::Stop),
963                    Err(error) => {
964                        visitor_error = Some(error);
965                        Ok(bedrock_leveldb::VisitorControl::Stop)
966                    }
967                }
968            });
969            match (scan_result, visitor_error) {
970                (_, Some(error)) => Err(error),
971                (Ok(outcome), None) => Ok(to_storage_outcome(outcome)),
972                (Err(error), None) => Err(map_leveldb_error(error)),
973            }
974        }
975
976        fn for_each_prefix_key(
977            &self,
978            prefix: &[u8],
979            options: StorageReadOptions,
980            visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
981        ) -> Result<StorageScanOutcome> {
982            let read_options = to_leveldb_read_options(options);
983            let mut visitor_error = None;
984            let scan_result =
985                self.db
986                    .for_each_prefix_key(prefix, read_options, |key| match visitor(key) {
987                        Ok(StorageVisitorControl::Continue) => {
988                            Ok(bedrock_leveldb::VisitorControl::Continue)
989                        }
990                        Ok(StorageVisitorControl::Stop) => {
991                            Ok(bedrock_leveldb::VisitorControl::Stop)
992                        }
993                        Err(error) => {
994                            visitor_error = Some(error);
995                            Ok(bedrock_leveldb::VisitorControl::Stop)
996                        }
997                    });
998            match (scan_result, visitor_error) {
999                (_, Some(error)) => Err(error),
1000                (Ok(outcome), None) => Ok(to_storage_outcome(outcome)),
1001                (Err(error), None) => Err(map_leveldb_error(error)),
1002            }
1003        }
1004
1005        fn write_batch(&self, batch: &StorageBatch) -> Result<()> {
1006            let mut db_batch = bedrock_leveldb::WriteBatch::new();
1007            for op in batch.ops() {
1008                match op {
1009                    StorageOp::Put { key, value } => db_batch.put(key.clone(), value.clone()),
1010                    StorageOp::Delete { key } => db_batch.delete(key.clone()),
1011                }
1012            }
1013            self.db
1014                .write(db_batch, write_options())
1015                .map_err(map_leveldb_error)
1016        }
1017
1018        fn flush(&self) -> Result<()> {
1019            Ok(())
1020        }
1021
1022        fn compact(&self) -> Result<()> {
1023            Ok(())
1024        }
1025    }
1026
1027    #[cfg(feature = "backend-bedrock-leveldb")]
1028    const fn write_options() -> bedrock_leveldb::WriteOptions {
1029        bedrock_leveldb::WriteOptions { sync: true }
1030    }
1031
1032    #[cfg(feature = "backend-bedrock-leveldb")]
1033    fn map_leveldb_error(error: bedrock_leveldb::LevelDbError) -> BedrockWorldError {
1034        match error.kind() {
1035            bedrock_leveldb::ErrorKind::Cancelled => BedrockWorldError::Cancelled {
1036                operation: "LevelDB scan",
1037            },
1038            bedrock_leveldb::ErrorKind::ReadOnly => BedrockWorldError::ReadOnly,
1039            _ => BedrockWorldError::LevelDb(error.to_string()),
1040        }
1041    }
1042
1043    #[cfg(feature = "backend-bedrock-leveldb")]
1044    fn to_leveldb_read_options(options: StorageReadOptions) -> bedrock_leveldb::ReadOptions {
1045        bedrock_leveldb::ReadOptions {
1046            checksum: bedrock_leveldb::ChecksumMode::Inherit,
1047            cache_policy: match options.cache_policy {
1048                StorageCachePolicy::Bypass => bedrock_leveldb::CachePolicy::Bypass,
1049                StorageCachePolicy::Use => bedrock_leveldb::CachePolicy::Use,
1050            },
1051            read_strategy: bedrock_leveldb::ReadStrategy::Shared,
1052            threading: match options.threading {
1053                StorageThreadingOptions::Auto => bedrock_leveldb::ThreadingOptions::Auto,
1054                StorageThreadingOptions::Fixed(threads) => {
1055                    bedrock_leveldb::ThreadingOptions::Fixed(threads)
1056                }
1057                StorageThreadingOptions::Single => bedrock_leveldb::ThreadingOptions::Single,
1058            },
1059            scan_mode: match options.scan_mode {
1060                StorageScanMode::Sequential => bedrock_leveldb::ScanMode::Sequential,
1061                StorageScanMode::ParallelTables => bedrock_leveldb::ScanMode::ParallelTables,
1062            },
1063            pipeline: bedrock_leveldb::ScanPipelineOptions {
1064                queue_depth: options.pipeline.queue_depth,
1065                table_batch_size: options.pipeline.table_batch_size,
1066                progress_interval: options.pipeline.progress_interval,
1067            },
1068            cancel: options
1069                .cancel
1070                .map(|cancel| bedrock_leveldb::ScanCancelFlag::from_shared(cancel.0)),
1071            progress: options.progress.map(|progress| {
1072                bedrock_leveldb::ScanProgressSink::new(move |db_progress| {
1073                    progress.emit(StorageScanProgress {
1074                        entries_seen: db_progress.visited,
1075                        bytes_read: db_progress.bytes_read,
1076                    });
1077                })
1078            }),
1079        }
1080    }
1081
1082    #[cfg(feature = "backend-bedrock-leveldb")]
1083    const fn to_storage_outcome(outcome: bedrock_leveldb::ScanOutcome) -> StorageScanOutcome {
1084        StorageScanOutcome {
1085            visited: outcome.visited,
1086            bytes_read: outcome.bytes_read,
1087            stopped: outcome.stopped,
1088            tables_scanned: outcome.tables_scanned,
1089            worker_threads: outcome.worker_threads,
1090            queue_wait_ms: outcome.queue_wait_ms,
1091            cancel_checks: outcome.cancel_checks,
1092        }
1093    }
1094
1095    #[cfg(not(feature = "backend-bedrock-leveldb"))]
1096    #[derive(Debug, Clone, Copy)]
1097    /// Placeholder backend returned when `backend-bedrock-leveldb` is disabled.
1098    pub struct BedrockLevelDbStorage;
1099
1100    #[cfg(not(feature = "backend-bedrock-leveldb"))]
1101    impl BedrockLevelDbStorage {
1102        /// Returns an error because the LevelDB backend feature is disabled.
1103        pub fn open(_path: impl AsRef<Path>) -> Result<Self> {
1104            Err(BedrockWorldError::LevelDb(
1105                "backend-bedrock-leveldb feature is disabled".to_string(),
1106            ))
1107        }
1108
1109        /// Returns an error because the LevelDB backend feature is disabled.
1110        pub fn open_read_only(_path: impl AsRef<Path>) -> Result<Self> {
1111            Err(BedrockWorldError::LevelDb(
1112                "backend-bedrock-leveldb feature is disabled".to_string(),
1113            ))
1114        }
1115
1116        /// Returns an error because the LevelDB backend feature is disabled.
1117        pub fn open_read_only_best_effort(_path: impl AsRef<Path>) -> Result<Self> {
1118            Err(BedrockWorldError::LevelDb(
1119                "backend-bedrock-leveldb feature is disabled".to_string(),
1120            ))
1121        }
1122    }
1123
1124    #[cfg(not(feature = "backend-bedrock-leveldb"))]
1125    impl WorldStorage for BedrockLevelDbStorage {
1126        fn get(&self, _key: &[u8]) -> Result<Option<Bytes>> {
1127            Err(BedrockWorldError::LevelDb(
1128                "backend-bedrock-leveldb feature is disabled".to_string(),
1129            ))
1130        }
1131
1132        fn get_many(&self, _keys: &[Bytes]) -> Result<Vec<Option<Bytes>>> {
1133            Err(BedrockWorldError::LevelDb(
1134                "backend-bedrock-leveldb feature is disabled".to_string(),
1135            ))
1136        }
1137
1138        fn put(&self, _key: &[u8], _value: &[u8]) -> Result<()> {
1139            Err(BedrockWorldError::LevelDb(
1140                "backend-bedrock-leveldb feature is disabled".to_string(),
1141            ))
1142        }
1143
1144        fn delete(&self, _key: &[u8]) -> Result<()> {
1145            Err(BedrockWorldError::LevelDb(
1146                "backend-bedrock-leveldb feature is disabled".to_string(),
1147            ))
1148        }
1149
1150        fn for_each_key(
1151            &self,
1152            _options: StorageReadOptions,
1153            _visitor: &mut (dyn FnMut(&[u8]) -> Result<StorageVisitorControl> + Send),
1154        ) -> Result<StorageScanOutcome> {
1155            Err(BedrockWorldError::LevelDb(
1156                "backend-bedrock-leveldb feature is disabled".to_string(),
1157            ))
1158        }
1159
1160        fn for_each_prefix(
1161            &self,
1162            _prefix: &[u8],
1163            _options: StorageReadOptions,
1164            _visitor: &mut (dyn FnMut(&[u8], &Bytes) -> Result<StorageVisitorControl> + Send),
1165        ) -> Result<StorageScanOutcome> {
1166            Err(BedrockWorldError::LevelDb(
1167                "backend-bedrock-leveldb feature is disabled".to_string(),
1168            ))
1169        }
1170
1171        fn write_batch(&self, _batch: &StorageBatch) -> Result<()> {
1172            Err(BedrockWorldError::LevelDb(
1173                "backend-bedrock-leveldb feature is disabled".to_string(),
1174            ))
1175        }
1176
1177        fn flush(&self) -> Result<()> {
1178            Err(BedrockWorldError::LevelDb(
1179                "backend-bedrock-leveldb feature is disabled".to_string(),
1180            ))
1181        }
1182
1183        fn compact(&self) -> Result<()> {
1184            Err(BedrockWorldError::LevelDb(
1185                "backend-bedrock-leveldb feature is disabled".to_string(),
1186            ))
1187        }
1188    }
1189}
1190
1191fn check_cancelled(options: &StorageReadOptions) -> Result<()> {
1192    if options
1193        .cancel
1194        .as_ref()
1195        .is_some_and(StorageCancelFlag::is_cancelled)
1196    {
1197        return Err(BedrockWorldError::Cancelled {
1198            operation: "storage scan",
1199        });
1200    }
1201    Ok(())
1202}
1203
1204fn emit_progress(options: &StorageReadOptions, outcome: StorageScanOutcome) {
1205    if let Some(progress) = &options.progress {
1206        progress.emit(StorageScanProgress {
1207            entries_seen: outcome.visited,
1208            bytes_read: outcome.bytes_read,
1209        });
1210    }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215    use super::*;
1216    #[cfg(feature = "backend-bedrock-leveldb")]
1217    use std::time::{SystemTime, UNIX_EPOCH};
1218
1219    #[test]
1220    fn memory_storage_scans_prefix_without_copying_values() {
1221        let storage = MemoryStorage::new();
1222        storage.put(b"abc1", b"one").expect("put");
1223        storage.put(b"abc2", b"two").expect("put");
1224        storage.put(b"abd", b"three").expect("put");
1225
1226        let mut entries = Vec::new();
1227        storage
1228            .for_each_prefix(b"abc", StorageReadOptions::default(), &mut |key, value| {
1229                entries.push(StorageEntry {
1230                    key: Bytes::copy_from_slice(key),
1231                    value: value.clone(),
1232                });
1233                Ok(StorageVisitorControl::Continue)
1234            })
1235            .expect("scan");
1236        assert_eq!(entries.len(), 2);
1237        assert_eq!(entries[0].key, Bytes::from_static(b"abc1"));
1238        assert_eq!(entries[1].value, Bytes::from_static(b"two"));
1239    }
1240
1241    #[cfg(feature = "backend-bedrock-leveldb")]
1242    #[test]
1243    fn bedrock_leveldb_storage_roundtrips_raw_records() {
1244        let path = std::env::temp_dir().join(format!(
1245            "bedrock-world-storage-{}",
1246            SystemTime::now()
1247                .duration_since(UNIX_EPOCH)
1248                .expect("time")
1249                .as_nanos()
1250        ));
1251        std::fs::create_dir_all(&path).expect("create");
1252        drop(
1253            bedrock_leveldb::Db::open(&path, bedrock_leveldb::OpenOptions::default())
1254                .expect("initialize"),
1255        );
1256
1257        let storage = backend::BedrockLevelDbStorage::open(&path).expect("open");
1258        storage.put(b"player_1", b"one").expect("put");
1259        storage.put(b"player_2", b"two").expect("put");
1260        storage.flush().expect("flush");
1261        let table_count = std::fs::read_dir(&path)
1262            .expect("read dir")
1263            .filter_map(std::result::Result::ok)
1264            .filter(|entry| {
1265                entry
1266                    .path()
1267                    .extension()
1268                    .is_some_and(|extension| extension == "ldb")
1269            })
1270            .count();
1271        assert_eq!(table_count, 0);
1272
1273        let reopened = backend::BedrockLevelDbStorage::open(&path).expect("reopen");
1274        assert_eq!(
1275            reopened.get(b"player_1").expect("get"),
1276            Some(Bytes::from_static(b"one"))
1277        );
1278        let mut player_count = 0;
1279        reopened
1280            .for_each_prefix(
1281                b"player_",
1282                StorageReadOptions::default(),
1283                &mut |_key, _value| {
1284                    player_count += 1;
1285                    Ok(StorageVisitorControl::Continue)
1286                },
1287            )
1288            .expect("scan");
1289        assert_eq!(player_count, 2);
1290
1291        std::fs::remove_dir_all(path).expect("cleanup");
1292    }
1293
1294    #[test]
1295    fn pocket_chunks_dat_exposes_virtual_legacy_terrain_records() {
1296        let path = std::env::temp_dir().join(format!(
1297            "bedrock-world-pocket-chunks-{}",
1298            std::time::SystemTime::now()
1299                .duration_since(std::time::UNIX_EPOCH)
1300                .expect("time")
1301                .as_nanos()
1302        ));
1303        std::fs::create_dir_all(&path).expect("create world dir");
1304        let mut terrain = vec![0_u8; POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN];
1305        let block_index = (1_usize << 11) | (3_usize << 7) | 2_usize;
1306        let column_index = 3_usize * 16 + 1_usize;
1307        terrain[block_index] = 42;
1308        terrain[crate::LEGACY_TERRAIN_BLOCK_COUNT
1309            + (crate::LEGACY_TERRAIN_BLOCK_COUNT / 2) * 3
1310            + column_index] = 99;
1311        let mut chunks = vec![0_u8; POCKET_CHUNKS_DAT_SECTOR_BYTES];
1312        chunks[0] = 21;
1313        chunks[1] = 1;
1314        let mut payload = Vec::new();
1315        payload.extend_from_slice(&(POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN as u32).to_le_bytes());
1316        payload.extend_from_slice(&terrain);
1317        chunks.extend_from_slice(&payload);
1318        let padded_len = POCKET_CHUNKS_DAT_SECTOR_BYTES * 22;
1319        chunks.resize(padded_len, 0);
1320        std::fs::write(path.join("chunks.dat"), chunks).expect("write chunks.dat");
1321
1322        let storage = PocketChunksDatStorage::open(&path).expect("open pocket chunks");
1323        let pos = ChunkPos {
1324            x: 0,
1325            z: 0,
1326            dimension: Dimension::Overworld,
1327        };
1328        let legacy_key = ChunkKey::new(pos, ChunkRecordTag::LegacyTerrain).encode();
1329        let missing_key = ChunkKey::new(
1330            ChunkPos {
1331                x: 1,
1332                z: 0,
1333                dimension: Dimension::Overworld,
1334            },
1335            ChunkRecordTag::LegacyTerrain,
1336        )
1337        .encode();
1338
1339        let values = storage
1340            .get_many(&[missing_key.clone(), legacy_key.clone()])
1341            .expect("get many");
1342        assert!(values[0].is_none());
1343        let Some(value) = &values[1] else {
1344            panic!("legacy terrain should be present");
1345        };
1346        assert_eq!(value.len(), LEGACY_TERRAIN_VALUE_LEN);
1347        assert_eq!(
1348            &value[..POCKET_CHUNKS_DAT_TERRAIN_VALUE_LEN],
1349            terrain.as_slice()
1350        );
1351        let terrain = crate::LegacyTerrain::parse(value.clone()).expect("legacy terrain");
1352        assert_eq!(terrain.block_id_at(1, 2, 3), Some(42));
1353        assert_eq!(terrain.height_at(1, 3), Some(99));
1354
1355        let mut keys = Vec::new();
1356        storage
1357            .for_each_key(StorageReadOptions::default(), &mut |key| {
1358                keys.push(Bytes::copy_from_slice(key));
1359                Ok(StorageVisitorControl::Continue)
1360            })
1361            .expect("scan keys");
1362        assert_eq!(keys, vec![legacy_key]);
1363        assert!(storage.put(b"x", b"y").is_err());
1364
1365        std::fs::remove_dir_all(path).expect("cleanup");
1366    }
1367}