feat(nwsync): emit blobs and per-artifact NSYM, assemble merged manifests
Adds `crucible nwsync emit` and `crucible nwsync assemble`, the split replacement for upstream nwn_nwsync_write, which cannot be used because it wants every hak, the TLK and the module present in one run on one disk. emit explodes one artifact — a .hak/.erf or a loose file such as the TLK — into NWSync blobs (NWCompressedBuffer framing, magic NSYC, zstd) plus a NSYM v3 manifest describing only that artifact, with the same .json sidecar upstream writes. Blob names are the sha1 of the uncompressed bytes, restypes nss/ndb/gic are always skipped, an unresolvable restype is a hard error, a resource over 15 MB fails closed, and no latest or .origin file is ever written. assemble merges the per-artifact manifests into one, reading no bulk data. The merge rule is resref shadowing, not concatenation: a resref present in more than one artifact resolves to the earliest artifact in --order, the way the game resolves it. It refuses to merge across mismatched emitter versions, and takes --group-id from the caller (1 current, 2 testing; 0 is absent). Refs #53.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
package nwsync
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
// NWCompressedBuffer framing, as upstream's neverwinter/compressedbuf.nim
|
||||
// writes it for NWSync blobs. All fields are little-endian uint32:
|
||||
//
|
||||
// magic "NSYC", version 3, algorithm 2 (zstd), uncompressed size,
|
||||
// zstd header version 1, dictionary 0, then the raw zstd frame.
|
||||
const (
|
||||
blobMagic = 0x4359534E // "NSYC" little-endian
|
||||
blobVersion = 3
|
||||
algorithmZstd = 2
|
||||
zstdHeaderVer = 1
|
||||
zstdDictionary = 0
|
||||
blobHeaderBytes = 24
|
||||
)
|
||||
|
||||
var (
|
||||
blobEncoder, _ = zstd.NewWriter(nil)
|
||||
blobDecoder, _ = zstd.NewReader(nil)
|
||||
)
|
||||
|
||||
// compressBlob wraps data in NWCompressedBuffer framing.
|
||||
func compressBlob(data []byte) []byte {
|
||||
var out bytes.Buffer
|
||||
header := []uint32{blobMagic, blobVersion, algorithmZstd, uint32(len(data)), zstdHeaderVer, zstdDictionary}
|
||||
for _, field := range header {
|
||||
_ = binary.Write(&out, binary.LittleEndian, field)
|
||||
}
|
||||
out.Write(blobEncoder.EncodeAll(data, nil))
|
||||
return out.Bytes()
|
||||
}
|
||||
|
||||
// decompressBlob unwraps NWCompressedBuffer framing. It exists so a blob this
|
||||
// package wrote — or one upstream wrote — can be compared by its uncompressed
|
||||
// bytes, which is the only comparison that is meaningful across zstd
|
||||
// implementations.
|
||||
func decompressBlob(blob []byte) ([]byte, error) {
|
||||
if len(blob) < blobHeaderBytes {
|
||||
return nil, fmt.Errorf("blob too small: %d bytes", len(blob))
|
||||
}
|
||||
header := make([]uint32, 6)
|
||||
if err := binary.Read(bytes.NewReader(blob[:blobHeaderBytes]), binary.LittleEndian, header); err != nil {
|
||||
return nil, fmt.Errorf("decode blob header: %w", err)
|
||||
}
|
||||
switch {
|
||||
case header[0] != blobMagic:
|
||||
return nil, fmt.Errorf("invalid blob magic: %#x", header[0])
|
||||
case header[1] != blobVersion:
|
||||
return nil, fmt.Errorf("unsupported blob version: %d", header[1])
|
||||
case header[2] != algorithmZstd:
|
||||
return nil, fmt.Errorf("unsupported compression algorithm: %d", header[2])
|
||||
case header[4] != zstdHeaderVer:
|
||||
return nil, fmt.Errorf("unsupported zstd header version: %d", header[4])
|
||||
case header[5] != zstdDictionary:
|
||||
return nil, fmt.Errorf("zstd dictionaries are not supported")
|
||||
}
|
||||
if header[3] == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
data, err := blobDecoder.DecodeAll(blob[blobHeaderBytes:], nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decompress blob: %w", err)
|
||||
}
|
||||
if uint32(len(data)) != header[3] {
|
||||
return nil, fmt.Errorf("blob size mismatch: header says %d, got %d", header[3], len(data))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user