denoland/deno · error · AnyError

malformed .msi: no _Columns stream

Error message

malformed .msi: no _Columns stream

What it means

The repacker sorts MSI system tables in place, then parses the _Columns table to recover each table's column widths and primary keys (needed to sort the remaining data tables). No _Columns stream means the database lacks its schema-definition table, so nothing downstream can be mapped — again a malformed-input signal, checked after the string streams were already found.

Solutions

  1. Start from a pristine template .msi from the original source; do not feed repacked output back as input.
  2. Inspect streams with a compound-file viewer (7-Zip can list them) and confirm _Columns exists alongside the other _-prefixed tables.
  3. If the pipeline intentionally strips tables, disable that step for the template used by the desktop installer build.
  4. Quarantine-check the file (antivirus logs) if corruption has no obvious cause.

Example fix

# before: repacked file fed back in as template
msi_repack out.msi out2.mi   # second pass over an already-repacked file
# error: malformed .msi: no _Columns stream

# after: always start from the pristine template
msi_repack template.msi out.msi
Defensive patterns

Strategy: validation

Validate before calling

7z l template.msi | grep -q '_Columns' || { echo 'MSI schema stream missing — template was stripped or corrupted'; exit 1; }

Try / catch

Catch and abort with a clear 'template unusable' message; a database without _Columns cannot be repacked and must be replaced from the pristine source, not retried.

Prevention

When it happens

Trigger: find_raw("_Columns") misses on a file that had _StringPool/_StringData: an MSI whose system tables were stripped or renamed by a prior tool, severe corruption, or an exotic authoring tool that omits it (standard ones never do).

Common situations: A previous repack/transform stripped system tables; the template passed through an optimizer that pruned streams; file corrupted in transfer or by antivirus quarantine.

Understand the failure class

Related errors


AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20). Data as JSON: /api/errors/d01b298f2fe97b97. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/desktop.rs:5344

    ("_Columns", vec![str_w, 2, str_w, 2], vec![0, 1]),
    (
      "_Validation",
      vec![str_w, str_w, str_w, 4, 4, str_w, 2, str_w, str_w, str_w],
      vec![0, 1],
    ),
  ];
  for (name, widths, keys) in system_tables {
    if let Some(raw) = find_raw(name) {
      let bytes = read_stream(&mut comp, &raw)?;
      let sorted = msi_resort_stream(&bytes, widths, keys);
      write_stream(&mut comp, &raw, &sorted)?;
    }
  }

  // Parse the (now sorted) `_Columns` table to recover every persistent table's
  // column widths and which columns are primary keys.
  let columns_raw = find_raw("_Columns").ok_or_else(|| {
    deno_core::anyhow::anyhow!("malformed .msi: no _Columns stream")
  })?;
  let columns = read_stream(&mut comp, &columns_raw)?;
  let col_row_w = str_w + 2 + str_w + 2; // Table, Number, Name, Type
  let ncol = columns.len() / col_row_w;
  let read_col = |arr_off: usize, row: usize, w: usize| -> u64 {
    let base = arr_off + row * w;
    let mut v = 0u64;
    for k in 0..w {
      v |= (columns[base + k] as u64) << (8 * k);
    }
    v
  };
  let off_table = 0usize;
  let off_number = ncol * str_w;
  let off_type = off_number + ncol * 2 + ncol * str_w;
  // A column Type word: 0x8000 marks the stored value present (always set here);
  // 0x0800 marks a string type; 0x2000 marks a primary-key column; for integer
  // columns the low byte is the storage size (2 or 4 bytes).

View on GitHub (pinned to f7822238ca)