mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
fix(sqlite): preserve column identity before row selection
Read raw column metadata through the built-in PRAGMA in the private empty temp schema. Validate bytes before names become SQL identifiers, and select inventory subsets by exact encoded identity so unrelated malformed names cannot alias valid columns. Validation: 5 focused identity controls; 15 selected producer/provider controls (33 skipped); public CLI baseline collision, fixed format refusal and valid-subset success; check:fast and docs:check. Generated inert SQLite fixtures on macOS arm64 with Node 24.18.0; no Windows qualification or large-file/stress workload. Refs #4710
This commit is contained in:
1 parent
77c8a1ba32
commit
748ac7f0df
3 files changed
+153
-17
No files matched your search
@@ -35,10 +35,10 @@ const tableRow = z.object({
|
||||
});
|
||||
const columnRow = z.object({
|
||||
cid: natural,
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
name: z.instanceof(Uint8Array),
|
||||
type: z.instanceof(Uint8Array),
|
||||
notnull: z.bigint(),
|
||||
dflt_value: z.string().nullable(),
|
||||
dflt_value: z.instanceof(Uint8Array).nullable(),
|
||||
pk: natural,
|
||||
hidden: z
|
||||
.bigint()
|
||||
@@ -186,27 +186,46 @@ const cell = (
|
||||
const readColumns = (
|
||||
db: DatabaseSync,
|
||||
name: string,
|
||||
budget: OutputBudget,
|
||||
context: {
|
||||
readonly budget: OutputBudget;
|
||||
readonly encoding: DatabaseEncoding;
|
||||
readonly selectedColumns?: readonly string[] | undefined;
|
||||
},
|
||||
): SqliteDatabase["schema"]["tables"][number]["columns"] => {
|
||||
const { budget, encoding, selectedColumns } = context;
|
||||
const columns: SqliteDatabase["schema"]["tables"][number]["columns"] = [];
|
||||
// Direct PRAGMA syntax cannot be shadowed by a stored table or view.
|
||||
// This private read-only connection has an empty temp schema. Qualifying the
|
||||
// built-in there prevents stored main tables/views from shadowing it; the
|
||||
// final argument selects main's metadata. Decode bytes before names become
|
||||
// SQL identifiers. Inventory-only row subsets compare exact encoded names
|
||||
// first, so an unrelated malformed identifier cannot alias a requested one.
|
||||
const selection =
|
||||
selectedColumns === undefined
|
||||
? ""
|
||||
: ` WHERE CAST(name AS BLOB) IN (${selectedColumns.map(() => "CAST(? AS BLOB)").join(", ")})`;
|
||||
for (const raw of db
|
||||
.prepare(`PRAGMA main.table_xinfo(${quoteIdentifier(name)})`)
|
||||
.iterate()) {
|
||||
.prepare(
|
||||
`SELECT cid, CAST(name AS BLOB) AS name, CAST(type AS BLOB) AS type, "notnull", CAST(dflt_value AS BLOB) AS dflt_value, pk, hidden FROM temp.pragma_table_xinfo(?, 'main')${selection}`,
|
||||
)
|
||||
.iterate(name, ...(selectedColumns ?? []))) {
|
||||
const value = columnRow.parse(raw);
|
||||
const column = {
|
||||
cid: value.cid,
|
||||
name: value.name,
|
||||
declared_type: value.type,
|
||||
name: decodeSchemaText(value.name, encoding, "column name"),
|
||||
declared_type: decodeSchemaText(value.type, encoding, "column type"),
|
||||
not_null: value.notnull !== 0n,
|
||||
default_sql: value.dflt_value,
|
||||
default_sql:
|
||||
value.dflt_value === null
|
||||
? null
|
||||
: decodeSchemaText(value.dflt_value, encoding, "column default"),
|
||||
primary_key_position: value.pk,
|
||||
hidden: value.hidden,
|
||||
};
|
||||
budget.retain(column);
|
||||
columns.push(column);
|
||||
}
|
||||
return columns;
|
||||
// Sort only retained facts, without copying untrusted text into native sorter records.
|
||||
return columns.sort((left, right) => left.cid - right.cid);
|
||||
};
|
||||
|
||||
const readSchema = (
|
||||
@@ -355,11 +374,11 @@ const readSchema = (
|
||||
listed.type !== "virtual" &&
|
||||
(scope !== "inventory" || input.table === value.name)
|
||||
) {
|
||||
const columns = readColumns(
|
||||
db,
|
||||
value.name,
|
||||
scope === "inventory" ? new OutputBudget() : budget,
|
||||
);
|
||||
const columns = readColumns(db, value.name, {
|
||||
budget: scope === "inventory" ? new OutputBudget() : budget,
|
||||
encoding,
|
||||
selectedColumns: scope === "inventory" ? input.columns : undefined,
|
||||
});
|
||||
if (input.table === value.name)
|
||||
selectedTable = { name: table.name, kind: table.kind, columns };
|
||||
if (scope !== "inventory") table.columns = columns;
|
||||
|
||||
Reference in new issue
Block a user