Help us shape the future of Go! Take the 2026 Go Developer Survey

wim

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 19 Imported by: 2

README

go-wim

A native Go library for writing Windows Imaging (.wim) files.

err := wim.CaptureDir(ctx, "tree", "out.wim", wim.ImageInfo{Name: "Image", Boot: true},
    wim.Options{Security: wim.UniformSecurity(sd)})

It walks a file tree, deduplicates streams by SHA-1, and emits the dentry tree, the security table, the blob table and the XML. Resources are stored uncompressed or LZX-compressed. The LZX codec is in lzx, works in both directions, and is usable on its own.

This package writes. It parses enough of the format internally to place what it emits, and the tests parse the output back, but there is no reader API.

Why

Go could read a WIM but not write one, and the LZX variant the format uses had no Go implementation in either direction. That variant is distinct from the cabinet LZX that Go and C libraries do implement: a fixed 32 KiB window, a different block-size encoding, per-chunk resets of the window, the Huffman trees and the recent-offset state, and an unconditional Intel E8 filter at a fixed magic size. A cabinet decoder misreads a WIM chunk from its first field on.

The per-chunk reset is the interesting property: it is what lets a reader decode any chunk of a resource without the ones before it, which is what makes a WIM randomly accessible.

Two things the format will let you get wrong

Both are legal per the format, pass verification under other tools, and produce an image Windows cannot use — one unreadable, one unmountable. This package refuses them instead.

The metadata resource's compression follows the header, not its own flag. A reader takes the codec from the WIM header and decodes the metadata with it, without consulting that resource's own compressed bit. An image declaring LZX while storing its metadata raw is therefore well-formed and passes wimlib-imagex verify, yet its metadata decodes to garbage — the file list, and with it everything the image contains, is unreadable. So a compressed image always compresses its metadata, even when that costs bytes.

Every dentry needs a security ID. A capture from a POSIX filesystem has no NT security data to read, and the natural result is security_id = -1 throughout — which Windows rejects at mount. Options.Security therefore has no default, and a SecurityFunc returning nil for any file is an error rather than a way to skip it. What belongs in a descriptor is not this package's business; it treats them as opaque bytes and deduplicates them by content.

Compatibility

This writes the general-purpose WIM format: version 0x00010D00, non-solid, LZX-compressed or uncompressed, 32 KiB chunks — the same format install.wim and boot.wim have used across Windows releases. The header it emits is byte-for-byte identical to the one Microsoft's own imagex writes.

Deliberately not written, all optional: LZMS and solid resources (the .esd format), XPRESS, integrity tables, reparse points, hard links, short names, alternate data streams, extended attributes.

Verification status, stated precisely:

header and structures match Microsoft imagex's output measured
every resource verified and applied back by wimlib measured
parsed by go-winio's independent reader measured
a written image boots a Windows installation measured
read by DISM/WIMGAPI inferred from format identity

Booting has been exercised on one Windows release and one architecture, so read that row as "this works" rather than "this works everywhere". The last row follows from emitting the documented version with the documented codec, and from the header matching imagex's byte for byte — but it has not been exercised directly, so it is listed as an inference rather than a result.

Determinism

The same tree produces the same bytes. Timestamps come from the tree or from Options.Timestamp, the GUID is derived from the image's content rather than randomised, and chunk results are placed by index rather than as workers finish — so Options.Concurrency changes only how long a capture takes.

Testing

There is no CI yet. The suite runs locally and is worth running:

go test ./...          # round trips against an independent reader
go test -race ./...    # the parallel compression path

Coverage leans on implementations that share no code with this one:

  • go-winio's WIM parser reads back what the writer produces — no external tool needed, so this runs anywhere.
  • wimlib, when wimcapture/wimapply/wimlib-imagex are on PATH, verifies every resource's SHA-1 and round-trips an image back to a tree. Tests skip cleanly without it.
  • Two environment-gated sweeps for bulk validation: GOWIM_CORPUS=<dir of .wim files> decodes every resource in a corpus and checks it against its recorded hash, and GOWIM_LZX_CORPUS=<dir> measures the compressor's ratio and throughput against real files.

Compression

Roughly 4.5% larger than wimlib on the same input (41.1% of raw against 39.3%), at about 80% of its single-threaded speed, and parallel across cores. The difference is the parse strategy — greedy with lazy matching here, near-optimal in wimlib — not the framing: the Huffman trees are 1.67% of output, and deeper match search buys 0.14%. Closing it needs a real optimal parser.

Provenance

An independent reimplementation from the documented WIM format.

  • The MIT-licensed go-winio WIM reader (Copyright (c) Microsoft Corporation) was consulted as a format reference only; no go-winio source is incorporated. It is also a test dependency, used as an independent reader.
  • The LZX decoder here was ported from the author's own C decoder in pe-wimmf, which is Apache-2.0 and remains so in that repository; this port is MIT by the same author.
  • wimlib is used only as a black-box oracle at test time. It is GPL, and is neither linked into nor distributed with this software.
  • Field values and structure layouts were measured from real images — wimlib's captures and Microsoft imagex's — rather than assumed.

No Microsoft binaries or Windows files are included in this repository.

License

Copyright © 2026 Nikita Radchenko

Licensed under the MIT License.

The library has no runtime dependencies; go-winio (MIT) is used by the tests only.

Disclaimer

go-wim is not affiliated with, endorsed by, or derived from the code of Microsoft. Windows is a trademark of Microsoft Corporation.

Documentation

Overview

Package wim writes Windows Imaging (.wim) files.

It captures a file tree into a WIM: walking the tree, deduplicating streams by SHA-1, and emitting the dentry tree, the security table, the blob table, and the XML — with no external imaging tool involved. Resources are stored uncompressed or LZX-compressed; the codec lives in the lzx subpackage and is usable on its own.

The package writes; it does not read. Enough of the format is parsed internally to place what it emits, and the tests parse the output back, but there is no reader API.

Two properties of the format are load-bearing and easy to get wrong, and this package refuses rather than lets you discover them at boot. A loader takes the metadata resource's compression from the WIM header rather than from that resource's own flag, so an image declaring a codec must compress its metadata too — the mismatch is legal, verifies clean under other tools, and produces an image Windows will not boot. And every dentry needs a security ID: an image whose dentries carry -1, which is what a POSIX capture naturally produces, is rejected at mount, which is why Options.Security has no default.

Index

Examples

Constants

View Source
const DefaultChunkSize = 32768

DefaultChunkSize is the uncompressed size of one chunk of a compressed resource, and the value every WIM of this version in the wild uses. It is also the LZX window, which is why the codec cannot code a larger one.

Variables

View Source
var ErrChunkSize = errors.New("wim: ChunkSize must be a power of two no larger than the LZX window")

ErrChunkSize reports a chunk size the codec cannot code against.

It is refused because the failure is otherwise silent and looks like success: the LZX window is one chunk, so a chunk larger than the window is declined by the codec and stored raw, and a resource of those can still come out a few bytes shorter than its input — earning the compressed flag while holding almost entirely uncompressed data. The image is well formed, reads back correctly, and is neither compressed nor readable by anything expecting the standard chunk size.

View Source
var ErrNoSecurity = errors.New("wim: Options.Security is required (an image with no security table is rejected by Windows)")

ErrNoSecurity reports a capture with no security descriptors. An image whose dentries carry no security ID is what a POSIX filesystem naturally yields — there is no NT security data to read — and it is what Windows rejects at mount, so it is refused at capture time rather than discovered when the image is used.

Functions

func Capture

func Capture(ctx context.Context, dst io.WriteSeeker, src fs.FS, info ImageInfo, opts Options) error

Capture writes src to dst as a single-image WIM. It is NewWriter + AddImage + Close.

Example

Capture an in-memory tree, which is all a Writer needs — any fs.FS will do.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"testing/fstest"

	"github.com/nradchenko/go-wim"
)

// A security descriptor is opaque to this package; a real caller supplies one its target
// Windows accepts. This is a minimal self-relative descriptor so the examples run.
var descriptor = []byte{
	0x01, 0x00, 0x00, 0x80,
	0x14, 0x00, 0x00, 0x00,
	0x20, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
}

func main() {
	src := fstest.MapFS{
		"boot.ini":            &fstest.MapFile{Data: []byte("[boot loader]\n")},
		"system32/kernel.dll": &fstest.MapFile{Data: []byte("MZ...")},
	}

	out, err := os.CreateTemp("", "example-*.wim")
	if err != nil {
		log.Fatal(err)
	}
	defer os.Remove(out.Name())
	defer out.Close()

	err = wim.Capture(context.Background(), out, src,
		wim.ImageInfo{Name: "Image"},
		wim.Options{
			Security: wim.UniformSecurity(descriptor),
			// Deduplicated by content, so one descriptor for every file costs one entry.
			Compression: wim.CompressLZX,
		},
	)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("captured")
}
Output:
captured

func CaptureDir

func CaptureDir(ctx context.Context, dir, out string, info ImageInfo, opts Options) error

CaptureDir captures the directory dir into a new WIM file at out, replacing it if it exists. A failed capture leaves no partial file behind.

Example

Capture a directory into a bootable single-image WIM.

package main

import (
	"context"
	"log"

	"github.com/nradchenko/go-wim"
)

// A security descriptor is opaque to this package; a real caller supplies one its target
// Windows accepts. This is a minimal self-relative descriptor so the examples run.
var descriptor = []byte{
	0x01, 0x00, 0x00, 0x80,
	0x14, 0x00, 0x00, 0x00,
	0x20, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
}

func main() {
	err := wim.CaptureDir(context.Background(), "tree", "out.wim",
		wim.ImageInfo{
			Name: "Image",
			Boot: true,
			Windows: &wim.WindowsInfo{
				Arch:    wim.ArchIntel,
				Version: wim.Version{Major: 5, Minor: 1, Build: 2600},
			},
		},
		wim.Options{Security: wim.UniformSecurity(descriptor)},
	)
	if err != nil {
		log.Fatal(err)
	}
}

Types

type Arch

type Arch int

Arch is the processor architecture recorded in <WINDOWS><ARCH>, using Windows' own PROCESSOR_ARCHITECTURE numbering.

const (
	ArchIntel Arch = 0 // x86
	ArchAMD64 Arch = 9 // x64
)

type Compression

type Compression int

Compression selects the codec a WIM's file resources are stored with.

const (
	// CompressLZX stores resources as LZX-compressed chunks. It is the codec WIMs of this
	// version carry, and the only one this package implements; XPRESS is not written.
	CompressLZX Compression = iota
	// CompressNone stores every resource uncompressed. Useful for tests and for inspecting
	// a capture; a whole PE image stored this way is legal but large.
	CompressNone
)

type ImageInfo

type ImageInfo struct {
	// Name is the image's <NAME>. It is what wimlib and DISM show as the image's identity.
	Name string
	// Description is the image's optional <DESCRIPTION>.
	Description string
	// Boot marks this image bootable: it becomes the WIM's boot index, and the header's
	// boot-metadata resource points at this image's metadata. A loader reads the image the
	// boot index names.
	Boot bool
	// Windows, when non-nil, adds the <WINDOWS> block describing the Windows installation
	// the image holds.
	Windows *WindowsInfo
}

ImageInfo describes an image being captured.

type Options

type Options struct {
	// Compression is the codec for file resources. Defaults to CompressLZX.
	Compression Compression

	// ChunkSize is the uncompressed size of one chunk of a compressed resource. Defaults to
	// DefaultChunkSize, which is what real images carry; setting anything else is for tests.
	// It must be a power of two no larger than the LZX window, which is what the codec can
	// code a chunk against (see ErrChunkSize).
	ChunkSize int

	// Security assigns a security descriptor to each captured file. It has no default: see
	// ErrNoSecurity.
	Security SecurityFunc

	// Timestamp, when non-zero, is recorded as every dentry's creation, access, and write
	// time in place of the source tree's own times. Set it for a byte-reproducible capture
	// of a tree whose timestamps vary.
	Timestamp time.Time

	// GUID is the WIM's identifier. A zero GUID is derived from the image's own content,
	// keeping a capture reproducible; wimlib randomises it.
	GUID [16]byte

	// Concurrency bounds the chunk compressions in flight. Zero means one per CPU. Chunks are
	// independent by construction — each resets the window, the trees and the recent offsets —
	// so this is the cheapest speed available, and the output is byte-identical whatever the
	// value: results are placed by chunk index, never in completion order.
	Concurrency int
}

Options configure a Writer. Security is required; the rest default to LZX, 32768-byte chunks, and timestamps taken from the captured tree.

type SecurityFunc

type SecurityFunc func(path string, d fs.DirEntry) []byte

SecurityFunc returns the self-relative NT security descriptor to record for the captured file at path — slash-separated and relative to the capture root, with "." for the root itself. It must return one for every file: a dentry left without a security ID carries -1, which is the wimlib-on-Linux shape Windows rejects at mount, and a capture holding even a few of them verifies clean and fails at boot. Returning nil is therefore an error, not a way to skip a file.

The writer deduplicates descriptors by content, so returning the same bytes for every file costs exactly one security-table entry.

Example

Give different files different descriptors.

package main

import (
	"io/fs"

	"github.com/nradchenko/go-wim"
)

// A security descriptor is opaque to this package; a real caller supplies one its target
// Windows accepts. This is a minimal self-relative descriptor so the examples run.
var descriptor = []byte{
	0x01, 0x00, 0x00, 0x80,
	0x14, 0x00, 0x00, 0x00,
	0x20, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
	0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x12, 0x00, 0x00, 0x00,
}

func main() {
	security := func(path string, d fs.DirEntry) []byte {
		if d.IsDir() {
			return directoryDescriptor
		}
		return descriptor
	}
	_ = wim.Options{Security: security}
}

var directoryDescriptor = descriptor

func UniformSecurity

func UniformSecurity(sd []byte) SecurityFunc

UniformSecurity returns a SecurityFunc that assigns sd to every captured file — the shape a PE build wants, where one system descriptor covers the whole image.

type Version

type Version struct {
	Major   int
	Minor   int
	Build   int
	SPBuild int
	SPLevel int
}

Version is a Windows build version, recorded in <WINDOWS><VERSION>, in the four parts that element carries: 5.1.2600.5512 is one such version.

type WindowsInfo

type WindowsInfo struct {
	Arch       Arch
	Version    Version
	SystemRoot string
}

WindowsInfo is an image's <WINDOWS> block. Zero fields are omitted from the XML.

type Writer

type Writer struct {
	// contains filtered or unexported fields
}

Writer builds a WIM into dst. Resources are written as they are captured and the header, blob table, and XML are finalised by Close, so dst must be seekable — the header carries offsets that are only known once every image has been written.

A Writer holds one blob table across every image it writes, which is the whole reason it is a type rather than a function: identical files, within an image or across images, are stored once and referenced by SHA-1.

func NewWriter

func NewWriter(dst io.WriteSeeker, opts Options) *Writer

NewWriter returns a Writer that builds a WIM into dst. Nothing is written until the first AddImage; Close must be called to produce a valid file.

func (*Writer) AddImage

func (w *Writer) AddImage(ctx context.Context, src fs.FS, info ImageInfo) error

AddImage captures the tree rooted at src as a new image described by info, appending its file resources and metadata resource to the WIM. Files are deduplicated against everything already written. Symlinks in src are an error rather than being followed, so a capture cannot silently duplicate a file or escape its root.

func (*Writer) Close

func (w *Writer) Close() error

Close writes the blob table and the XML data, rewrites the header now that their offsets are known, and releases the Writer. It does not close dst.

Directories

Path Synopsis
Package lzx implements the WIM variant of the LZX compression algorithm, in both directions: a compressor for building WIMs and a decompressor the round-trip tests check it against.
Package lzx implements the WIM variant of the LZX compression algorithm, in both directions: a compressor for building WIMs and a decompressor the round-trip tests check it against.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL