denoland/deno · error · AnyError
malformed .msi: no _StringPool stream
Error message
malformed .msi: no _StringPool stream
What it means
deno desktop's Windows installer repacks an existing .msi by directly editing its OLE compound-file streams (after de-mangling stream names). _StringPool is the MSI string-table header stream every valid MSI must contain; its absence means the input is not a well-formed MSI database — wrong file type, truncated template, or stream names mangled beyond what msi_demangle_stream_name can undo.
Solutions
- Verify the input is a real MSI database: `file template.msi` (should say 'Composite Document File') and open it with lessmsi or msidb to confirm streams exist.
- Re-obtain the pristine .msi template (re-download/rebuild) — a valid MSI always has _StringPool and _StringData.
- If the template is custom-built, generate it with a standard tool (WiX) so the system-table streams exist.
- Check that no earlier repack step already rewrote stream names into a form the demangler cannot reverse.
Example fix
# before: wrong file handed to the repack file installer/template.msi # PE32 executable (GUI) -> it is a bootstrapper .exe, not an .msi # after: supply the real database file installer/template.msi # Composite Document File V2 Document -> OK, re-run the build
Defensive patterns
Strategy: validation
Validate before calling
# Validate the template is a real MSI with the expected system streams before repacking
file template.msi | grep -q 'Composite Document File' || { echo 'not an OLE/MSI file'; exit 1; }
7z l template.msi | grep -q '_StringPool' || { echo 'no _StringPool stream'; exit 1; } Try / catch
Catch the 'malformed .msi' family, re-check the file signature out-of-band, and fail the installer build with 'template is not a valid MSI' — retrying the same input is pointless since the file on disk will not change.
Prevention
- Fetch installer templates from a checksum-pinned source and verify size/digest before use.
- Never feed bootstrapper .exe files or previously repacked outputs where a database .msi is expected.
- Keep the deno desktop repack pipeline single-pass: pristine template in, installer out.
When it happens
Trigger: find_raw("_StringPool") finds no stream whose de-mangled name ends with _StringPool: passing a non-MSI file as the installer template, a corrupted/truncated .msi, or an .msi produced by a tool that names/encodes streams in an unexpected way.
Common situations: The bundled installer template was replaced or corrupted in a fork; a download of the template was truncated; the file is actually an .exe bootstrapper rather than the .msi database; a repack pipeline fed the output of a previous failed run back in as input.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed .msi: no _Columns stream
- malformed .msi: no _StringData stream
- cabinet file missing
- Cannot build a .msi from an empty app directory
- deno.json `version` " " cannot be used as an MSI…
AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20).
Data as JSON: /api/errors/ec16d87ff6f3768b.
Report an issue: GitHub.
Appendix: source
Thrown at cli/tools/desktop.rs:5291
let write_stream = |comp: &mut cfb::CompoundFile<std::fs::File>,
raw: &str,
bytes: &[u8]|
-> Result<(), AnyError> {
let mut s = comp.open_stream(format!("/{raw}"))?;
s.write_all(bytes)?;
Ok(())
};
let find_raw = |suffix: &str| -> Option<String> {
names
.iter()
.find(|n| msi_demangle_stream_name(n).ends_with(suffix))
.cloned()
};
// The string-pool header's top bit selects 3-byte string references; tiny
// databases use 2-byte refs, but honor the flag to stay correct at any size.
let pool_raw = find_raw("_StringPool").ok_or_else(|| {
deno_core::anyhow::anyhow!("malformed .msi: no _StringPool stream")
})?;
let pool = read_stream(&mut comp, &pool_raw)?;
let str_w: usize = if pool.len() >= 4 && (pool[3] & 0x80) != 0 {
3
} else {
2
};
// Map every string-pool id to its text so data-table streams (named by table
// text) can be matched to their column schema (keyed by table string id).
let data_raw = find_raw("_StringData").ok_or_else(|| {
deno_core::anyhow::anyhow!("malformed .msi: no _StringData stream")
})?;
let str_data = read_stream(&mut comp, &data_raw)?;
let mut strings = vec![String::new()]; // 1-based; index 0 is unused.
{
let mut off = 0usize;
let mut i = 4;View on GitHub (pinned to f7822238ca)