298 lines
6.8 KiB
Go
298 lines
6.8 KiB
Go
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
|
|
}
|