package erf import ( "bytes" "encoding/binary" "fmt" "io" "sort" "strings" ) const ( headerSize = 160 versionV10 = "V1.0" strRefNone = 0xFFFFFFFF typeMOD = "MOD " typeHAK = "HAK " ) type Archive struct { FileType string Version string Resources []Resource } type Resource struct { Name string Type uint16 Data []byte } type header struct { FileType [4]byte Version [4]byte LanguageCount uint32 LocalizedStringSize uint32 EntryCount uint32 LocalizedStringOffset uint32 KeyListOffset uint32 ResourceListOffset uint32 BuildYear uint32 BuildDay uint32 DescriptionStrRef uint32 Reserved [116]byte } type keyEntry struct { ResRef [16]byte ResourceID uint32 ResourceType uint16 Unused uint16 } type resourceEntry struct { Offset uint32 Size uint32 } var extensionTypes = map[string]uint16{ "res": 0x0000, "bmp": 0x0001, "mve": 0x0002, "tga": 0x0003, "wav": 0x0004, "plt": 0x0006, "ini": 0x0007, "txt": 0x000A, "mdl": 0x07D2, "nss": 0x07D9, "ncs": 0x07DA, "are": 0x07DC, "set": 0x07DD, "ifo": 0x07DE, "bic": 0x07DF, "wok": 0x07E1, "2da": 0x07E2, "tlk": 0x07E3, "txi": 0x07E6, "git": 0x07E7, "uti": 0x07E9, "utt": 0x07EA, "utc": 0x07EB, "dlg": 0x07ED, "itp": 0x07EE, "uts": 0x07F0, "dds": 0x07F1, "fac": 0x07F6, "gff": 0x07F7, "ute": 0x07F9, "utd": 0x07FB, "utp": 0x07FC, "dfa": 0x07FD, "gic": 0x07FE, "gui": 0x07FF, "utm": 0x0800, "dwk": 0x0804, "pwk": 0x0805, "jrl": 0x0808, "utw": 0x080A, "ssf": 0x080C, "hak": 0x080D, "nwm": 0x080E, "bik": 0x080F, "ndb": 0x0810, "ptm": 0x0811, "ptt": 0x0812, "ltr": 0x0813, "shd": 0x0815, "mtr": 0x0818, "lod": 0x081E, } var typeExtensions map[uint16]string func init() { typeExtensions = make(map[uint16]string, len(extensionTypes)) for ext, resourceType := range extensionTypes { if _, exists := typeExtensions[resourceType]; !exists { typeExtensions[resourceType] = ext } } } func New(fileType string, resources []Resource) Archive { normalized := make([]Resource, len(resources)) copy(normalized, resources) sort.Slice(normalized, func(i, j int) bool { if normalized[i].Name == normalized[j].Name { return normalized[i].Type < normalized[j].Type } return normalized[i].Name < normalized[j].Name }) return Archive{ FileType: padFour(fileType), Version: versionV10, Resources: normalized, } } func ArchiveSize(resources []Resource) int64 { return int64(headerSize + len(resources)*24 + len(resources)*8 + totalResourceBytes(resources)) } func Write(w io.Writer, archive Archive) error { if archive.Version == "" { archive.Version = versionV10 } if archive.FileType == "" { archive.FileType = typeMOD } keys := make([]keyEntry, 0, len(archive.Resources)) entries := make([]resourceEntry, 0, len(archive.Resources)) dataOffset := uint32(headerSize + len(archive.Resources)*24 + len(archive.Resources)*8) data := bytes.Buffer{} for index, resource := range archive.Resources { if len(resource.Name) > 16 { return fmt.Errorf("resource %q exceeds 16-byte resref limit", resource.Name) } entry := resourceEntry{ Offset: dataOffset + uint32(data.Len()), Size: uint32(len(resource.Data)), } entries = append(entries, entry) var key keyEntry copy(key.ResRef[:], []byte(resource.Name)) key.ResourceID = uint32(index) key.ResourceType = resource.Type keys = append(keys, key) if _, err := data.Write(resource.Data); err != nil { return err } } hdr := header{ LanguageCount: 0, LocalizedStringSize: 0, EntryCount: uint32(len(archive.Resources)), LocalizedStringOffset: headerSize, KeyListOffset: headerSize, ResourceListOffset: uint32(headerSize + len(keys)*24), BuildYear: 0, BuildDay: 0, DescriptionStrRef: strRefNone, } copy(hdr.FileType[:], []byte(padFour(archive.FileType))) copy(hdr.Version[:], []byte(padFour(archive.Version))) var buf bytes.Buffer if err := binary.Write(&buf, binary.LittleEndian, hdr); err != nil { return err } if err := binary.Write(&buf, binary.LittleEndian, keys); err != nil { return err } if err := binary.Write(&buf, binary.LittleEndian, entries); err != nil { return err } if _, err := buf.Write(data.Bytes()); err != nil { return err } _, err := w.Write(buf.Bytes()) return err } func Read(r io.Reader) (Archive, error) { data, err := io.ReadAll(r) if err != nil { return Archive{}, fmt.Errorf("read erf: %w", err) } if len(data) < headerSize { return Archive{}, fmt.Errorf("erf file too small: %d bytes", len(data)) } var hdr header if err := binary.Read(bytes.NewReader(data[:headerSize]), binary.LittleEndian, &hdr); err != nil { return Archive{}, fmt.Errorf("decode erf header: %w", err) } keyStart := int(hdr.KeyListOffset) keyEnd := keyStart + int(hdr.EntryCount)*24 if keyEnd > len(data) { return Archive{}, fmt.Errorf("erf key list exceeds file bounds") } keys := make([]keyEntry, hdr.EntryCount) if err := binary.Read(bytes.NewReader(data[keyStart:keyEnd]), binary.LittleEndian, &keys); err != nil { return Archive{}, fmt.Errorf("decode key list: %w", err) } resourceStart := int(hdr.ResourceListOffset) resourceEnd := resourceStart + int(hdr.EntryCount)*8 if resourceEnd > len(data) { return Archive{}, fmt.Errorf("erf resource list exceeds file bounds") } entries := make([]resourceEntry, hdr.EntryCount) if err := binary.Read(bytes.NewReader(data[resourceStart:resourceEnd]), binary.LittleEndian, &entries); err != nil { return Archive{}, fmt.Errorf("decode resource list: %w", err) } resources := make([]Resource, 0, hdr.EntryCount) for index, key := range keys { entry := entries[index] start := int(entry.Offset) end := start + int(entry.Size) if end > len(data) { return Archive{}, fmt.Errorf("resource %d exceeds file bounds", index) } resref := string(bytes.TrimRight(key.ResRef[:], "\x00")) payload := make([]byte, entry.Size) copy(payload, data[start:end]) resources = append(resources, Resource{ Name: resref, Type: key.ResourceType, Data: payload, }) } return Archive{ FileType: string(hdr.FileType[:]), Version: string(hdr.Version[:]), Resources: resources, }, nil } func ResourceTypeForExtension(extension string) (uint16, bool) { resourceType, ok := extensionTypes[strings.TrimPrefix(strings.ToLower(extension), ".")] return resourceType, ok } func ExtensionForResourceType(resourceType uint16) (string, bool) { extension, ok := typeExtensions[resourceType] return extension, ok } func padFour(value string) string { value = strings.ToUpper(value) if len(value) >= 4 { return value[:4] } return value + strings.Repeat(" ", 4-len(value)) } func totalResourceBytes(resources []Resource) int { total := 0 for _, resource := range resources { total += len(resource.Data) } return total }