Prepare a stripped DB copy for best-quality export. * VACUUM INTO → (optionally) drop indexes → VACUUM. Returns malloc'd buffer * or NULL. VACUUM INTO runs on BOTH quality levels: it is the consistent * snapshot — the store runs in WAL mode, so raw main-file bytes miss * committed transactions still in the -wal and can be mid-checkpoint torn * (#895). Only the index-stripping is BEST-only. */
| 485 | * committed transactions still in the -wal and can be mid-checkpoint torn |
| 486 | * (#895). Only the index-stripping is BEST-only. */ |
| 487 | static char *prepare_snapshot_db(const char *db_path, size_t *out_size, bool strip_indexes) { |
| 488 | artifact_snapshot_tmp_t tmp; |
| 489 | if (!artifact_snapshot_tmp_open(&tmp)) { |
| 490 | artifact_export_fail("prepare_snapshot_dir", cbm_tmpdir(), "private_tmpdir_failed", errno); |
| 491 | return NULL; |
| 492 | } |
| 493 | /* Fresh private directory ⇒ the destination is absent by construction, which |
| 494 | * is exactly what VACUUM INTO requires. The old unlink-the-stale-file step |
| 495 | * is gone with the fixed name it existed to clear. */ |
| 496 | const char *tmp_path = tmp.db; |
| 497 | |
| 498 | /* VACUUM INTO: clean compacted copy. Use raw sqlite3 to bypass store authorizer |
| 499 | * (which blocks ATTACH, used internally by VACUUM INTO). */ |
| 500 | sqlite3 *raw_db = NULL; |
| 501 | if (sqlite3_open_v2(db_path, &raw_db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) { |
| 502 | const char *err = raw_db ? sqlite3_errmsg(raw_db) : "sqlite_open"; |
| 503 | artifact_export_fail("open_source_db", db_path, err, 0); |
| 504 | sqlite3_close(raw_db); |
| 505 | artifact_snapshot_tmp_close(&tmp); |
| 506 | return NULL; |
| 507 | } |
| 508 | |
| 509 | char vacuum_sql[CBM_SZ_4K]; |
| 510 | snprintf(vacuum_sql, sizeof(vacuum_sql), "VACUUM INTO '%s';", tmp_path); |
| 511 | char *errmsg = NULL; |
| 512 | int vrc = sqlite3_exec(raw_db, vacuum_sql, NULL, NULL, &errmsg); |
| 513 | sqlite3_close(raw_db); |
| 514 | |
| 515 | if (vrc != SQLITE_OK) { |
| 516 | artifact_export_fail("vacuum_into", tmp_path, errmsg ? errmsg : sqlite3_errstr(vrc), 0); |
| 517 | sqlite3_free(errmsg); |
| 518 | artifact_snapshot_tmp_close(&tmp); |
| 519 | return NULL; |
| 520 | } |
| 521 | |
| 522 | /* Strip indexes from the copy for better compression (BEST only). */ |
| 523 | if (strip_indexes) { |
| 524 | sqlite3 *tmp_db = NULL; |
| 525 | if (sqlite3_open_v2(tmp_path, &tmp_db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK) { |
| 526 | sqlite3_exec(tmp_db, DROP_INDEXES_SQL, NULL, NULL, NULL); |
| 527 | sqlite3_exec(tmp_db, "VACUUM;", NULL, NULL, NULL); |
| 528 | sqlite3_close(tmp_db); |
| 529 | } |
| 530 | } |
| 531 | |
| 532 | /* Reopened by path rather than held on a descriptor across VACUUM INTO, |
| 533 | * because sqlite owns the create. The private directory is what makes that |
| 534 | * safe: an attacker who cannot enter it cannot swap the file underneath. */ |
| 535 | char *data = read_file_alloc(tmp_path, out_size); |
| 536 | if (!data || *out_size == 0) { |
| 537 | artifact_export_fail("read_stripped_db", tmp_path, "empty_or_unreadable", errno); |
| 538 | } |
| 539 | /* Removes the copy, its WAL/SHM sidecars, and the private directory. */ |
| 540 | artifact_snapshot_tmp_close(&tmp); |
| 541 | return data; |
| 542 | } |
| 543 | |
| 544 | /* ── Export ───────────────────────────────────────────────────────── */ |
no test coverage detected