Repository: cberner/redb (Rust)

Issue:
Crash on startup after multiple runs
I was trying to create a minimum reproduction for a different data corruption issue i've been having when playing with redb, but found this instead.

reproduction:
```rust
const TABLE: redb::MultimapTableDefinition<'static, u128, u128> =
    redb::MultimapTableDefinition::new("some-table");

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut db = redb::Database::create("db.redb")?;
    db.check_integrity()?;

    let txn = db.begin_write()?;
    let mut table = txn.open_multimap_table(TABLE)?;

    for i in 0..1024 {
        table.insert(0, i)?;
    }
    drop(table);

    txn.commit()?;

    Ok(())
}
```

error:
```
thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `1089536`,
 right: `1589248`', /home/asonix/.cargo/registry/src/index.crates.io-6f17d22bba15001f/redb-1.0.2/src/tree_store/page_store/page_manager.rs:244:9
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::assert_failed_inner
   3: core::panicking::assert_failed
             at /build/rustc-1.70.0-src/library/core/src/panicking.rs:228:5
   4: redb::tree_store::page_store::page_manager::TransactionalMemory::new
             at /home/asonix/.cargo/registry/src/index.crates.io-6f17d22bba15001f/redb-1.0.2/src/tree_store/page_store/page_manager.rs:244:9
   5: redb::db::Database::new
             at /home/asonix/.cargo/registry/src/index.crates.io-6f17d22bba15001f/redb-1.0.2/src/db.rs:569:23
   6: redb::db::Builder::create
             at /home/asonix/.cargo/registry/src/index.crates.io-6f17d22bba15001f/redb-1.0.2/src/db.rs:746:9
   7: redb::db::Database::create
             at /home/asonix/.cargo/registry/src/index.crates.io-6f17d22bba15001f/redb-1.0.2/src/db.rs:249:9
   8: redb_bug::main
             at ./src/main.rs:5:18
   9: core::ops::function::FnOnce::call_once
             at /build/rustc-1.70.0-src/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
```


Discussion:
It doesn't crash on the first run or the second run, but seems to always crash on the third run when opening the database
Thanks for the simple repro! I'll take a look this weekend
Same thing. Here is another simple example. If `compact()` is called after check_integrity(), it fails as above on the 2nd start instead of 3rd.

```
use redb::{Database, Error, ReadableTable, TableDefinition};

const TABLE: TableDefinition<&str, u64> = TableDefinition::new("my_data");

fn main() -> Result<(), Error> {
    let mut db = Database::create("my_db.redb")?;
    let _ = db.check_integrity()?;
    // let _ = db.compact()?;
    let write_txn = db.begin_write()?;
    {
        let mut table = write_txn.open_table(TABLE)?;
        table.insert("my_key", &123)?;
    }
    write_txn.commit()?;

    let read_txn = db.begin_read()?;
    let table = read_txn.open_table(TABLE)?;
    assert_eq!(table.get("my_key")?.unwrap().value(), 123);

    Ok(())
}
```
Fix turned out to be pretty easy. I believe the attached PR will solve both these cases



Patch:
diff --git a/src/tree_store/page_store/page_manager.rs b/src/tree_store/page_store/page_manager.rs
index 13a64716..24c87c66 100644
--- a/src/tree_store/page_store/page_manager.rs
+++ b/src/tree_store/page_store/page_manager.rs
@@ -110,36 +110,6 @@ impl TransactionalMemory {
         let region_size = min(region_size, (MAX_PAGE_INDEX as u64 + 1) * page_size as u64);
         assert!(region_size.is_power_of_two());
 
-        let region_tracker_required_bytes =
-            RegionTracker::new(INITIAL_REGIONS, MAX_MAX_PAGE_ORDER + 1)
-                .to_vec()
-                .len();
-
-        // Make sure that there is enough room to allocate the region tracker into a page
-        let size: u64 = max(
-            MIN_DESIRED_USABLE_BYTES,
-            page_size as u64 * MIN_USABLE_PAGES as u64,
-        );
-        let tracker_space =
-            (page_size * ((region_tracker_required_bytes + page_size - 1) / page_size)) as u64;
-        let starting_size = size + tracker_space;
-
-        let layout = DatabaseLayout::calculate(
-            starting_size,
-            (region_size / u64::try_from(page_size).unwrap())
-                .try_into()
-                .unwrap(),
-            page_size.try_into().unwrap(),
-        );
-
-        {
-            let file_len = file.metadata()?.len();
-
-            if file_len < layout.len() {
-                file.set_len(layout.len())?;
-            }
-        }
-
         let mut storage = PagedCachedFile::new(
             file,
             page_size as u64,
@@ -147,12 +117,47 @@ impl TransactionalMemory {
             write_cache_size_bytes,
         )?;
 
-        let magic_number: [u8; MAGICNUMBER.len()] = storage
-            .read_direct(0, MAGICNUMBER.len())?
-            .try_into()
-            .unwrap();
+        let magic_number: [u8; MAGICNUMBER.len()] =
+            if storage.raw_file_len()? >= MAGICNUMBER.len() as u64 {
+                storage
+                    .read_direct(0, MAGICNUMBER.len())?
+                    .try_into()
+                    .unwrap()
+            } else {
+                [0; MAGICNUMBER.len()]
+            };
 
         if magic_number != MAGICNUMBER {
+            let region_tracker_required_bytes =
+                RegionTracker::new(INITIAL_REGIONS, MAX_MAX_PAGE_ORDER + 1)
+                    .to_vec()
+                    .len();
+
+            // Make sure that there is enough room to allocate the region tracker into a page
+            let size: u64 = max(
+                MIN_DESIRED_USABLE_BYTES,
+                page_size as u64 * MIN_USABLE_PAGES as u64,
+            );
+            let tracker_space =
+                (page_size * ((region_tracker_required_bytes + page_size - 1) / page_size)) as u64;
+            let starting_size = size + tracker_space;
+
+            let layout = DatabaseLayout::calculate(
+                starting_size,
+                (region_size / u64::try_from(page_size).unwrap())
+                    .try_into()
+                    .unwrap(),
+                page_size.try_into().unwrap(),
+            );
+
+            {
+                let file_len = storage.raw_file_len()?;
+
+                if file_len < layout.len() {
+                    storage.resize(layout.len())?;
+                }
+            }
+
             let mut allocators = Allocators::new(layout);
 
             // Allocate the region tracker in the zeroth region
@@ -209,7 +214,9 @@ impl TransactionalMemory {
             return Err(DatabaseError::UpgradeRequired(version));
         }
 
-        let needs_recovery = header.recovery_required;
+        assert!(storage.raw_file_len()? >= header.layout().len());
+        let needs_recovery =
+            header.recovery_required || header.layout().len() != storage.raw_file_len()?;
         if needs_recovery {
             let layout = header.layout();
             let region_max_pages = layout.full_region_layout().num_pages();


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 05bf3518..3c5174c0 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -817,6 +817,29 @@ fn regression19() {
     tx.open_table(table_def).unwrap();
 }
 
+#[test]
+fn regression20() {
+    let tmpfile = create_tempfile();
+
+    let table_def: MultimapTableDefinition<'static, u128, u128> =
+        MultimapTableDefinition::new("some-table");
+
+    for _ in 0..3 {
+        let mut db = Database::builder().create(tmpfile.path()).unwrap();
+        db.check_integrity().unwrap();
+
+        let txn = db.begin_write().unwrap();
+        let mut table = txn.open_multimap_table(table_def).unwrap();
+
+        for i in 0..1024 {
+            table.insert(0, i).unwrap();
+        }
+        drop(table);
+
+        txn.commit().unwrap();
+    }
+}
+
 #[test]
 fn no_savepoint_resurrection() {
     let tmpfile = create_tempfile();