diff --git a/block_save_container.go b/block_save_container.go new file mode 100644 index 0000000..a76cfdf --- /dev/null +++ b/block_save_container.go @@ -0,0 +1,23 @@ +package sii + +func init() { + RegisterBlock(&SaveContainer{}) +} + +type SaveContainer struct { + SaveName string `sii:"name"` + Time int64 `sii:"time"` + FileTime uint64 `sii:"file_time"` + Version int `sii:"version"` + Dependencies []string `sii:"dependencies"` + + blockName string +} + +func (SaveContainer) Class() string { return "save_container" } + +func (s *SaveContainer) Init(class, name string) { + s.blockName = name +} + +func (s SaveContainer) Name() string { return s.blockName } diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..b8d2abb --- /dev/null +++ b/parser.go @@ -0,0 +1,138 @@ +package sii + +import ( + "bufio" + "bytes" + "io" + "reflect" + "regexp" + "strings" + + "github.com/pkg/errors" +) + +var blockStartRegex = regexp.MustCompile(`^([^\s:]+)\s?:\s?([^\s]+)(?:\s?\{)?$`) + +func parseSIIPlainFile(r io.Reader) (*Unit, error) { + var ( + blockContent []byte + blockName string + blockClass string + inBlock = false + inComment = false + inUnit = false + scanner = bufio.NewScanner(r) + unit = &Unit{} + ) + + for scanner.Scan() { + var line = strings.TrimSpace(scanner.Text()) + + switch { + + case line == "{": + if !inUnit { + inUnit = true + continue + } + + if !inBlock { + if blockClass == "" || blockName == "" { + return nil, errors.New("Unpexpected block open without unit class / name") + } + + inBlock = true + continue + } + + return nil, errors.New("Unexpected opening braces") + + case line == "}": + if inBlock { + if err := processBlock(unit, blockClass, blockName, blockContent); err != nil { + return nil, errors.Wrap(err, "Unable to process block") + } + + inBlock = false + blockClass = "" + blockName = "" + continue + } + + if inUnit { + inUnit = false + continue + } + + return nil, errors.New("Unexpected closing braces") + + case blockStartRegex.MatchString(line) && !inBlock: + if !inUnit { + return nil, errors.New("Unexpected block start outside unit") + } + + groups := blockStartRegex.FindStringSubmatch(line) + + blockClass = groups[1] + blockName = groups[2] + + if strings.HasSuffix(line, `{`) { + inBlock = true + } + + case (strings.HasPrefix(line, `\*`) && strings.HasSuffix(line, `*\`)) || strings.HasPrefix(line, `#`) || strings.HasPrefix(line, `//`): + // one-line-comment, just drop + + case strings.HasPrefix(line, `/*`): + inComment = true + + case strings.HasSuffix(line, `*/`): + inComment = false + + default: + if inComment { + // Inside multi-line-comment, just drop + continue + } + + if !inBlock { + // Outside block, drop line + continue + } + + // Append line to block content + blockContent = bytes.Join([][]byte{ + blockContent, + scanner.Bytes(), + }, []byte{'\n'}) + + } + + } + + if scanner.Err() != nil { + return nil, errors.Wrap(scanner.Err(), "Unable to scan file") + } + + return unit, nil +} + +func processBlock(unit *Unit, blockClass, blockName string, blockContent []byte) error { + block := getBlockInstance(blockClass) + block.Init(blockClass, blockName) + + var err error + if reflect.TypeOf(block).Implements(reflect.TypeOf((*Unmarshaler)(nil)).Elem()) { + err = block.(Unmarshaler).UnmarshalSII(blockContent) + } else { + // TODO: Add generic unmarshal + } + + if err != nil { + return errors.Wrap(err, "Unable to unmarshal block content") + } + + unit.Entries = append(unit.Entries, block) + + return nil +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..de31f79 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,64 @@ +package sii + +import ( + "strings" + "testing" +) + +var testSii = `SiiNunit +{ +/* + * Multi-line comment + */ +some_unit : .my_mod.unit +{ + // Single line comment + # Single line comment + /* + * In-Block multi-line string + */ + attribute_number: 40 + attribute_string: "TEST STRING" + attribute_unit: test.unit + attribute_float3: (1.0, 1.0, 1.0) + attribute_float_number_ieee754: &40490f5a +} + +save_container : _nameless.1c57,b4b0 { + name: "" + time: 96931 + file_time: 1572907597 + version: 42 + dependencies: 14 + dependencies[0]: "mod|promods-assets-v242|ProMods Assets Package" + dependencies[1]: "mod|promods-model1-v242|ProMods Models Package 1" + dependencies[2]: "mod|promods-model2-v242|ProMods Models Package 2" + dependencies[3]: "mod|promods-model3-v242|ProMods Models Package 3" + dependencies[4]: "mod|promods-media-v242|ProMods Media Package" + dependencies[5]: "mod|promods-map-v242|ProMods Map Package" + dependencies[6]: "mod|promods-def-v242|ProMods Definition Package" + dependencies[7]: "dlc|eut2_balt|DLC - Beyond the Baltic Sea" + dependencies[8]: "dlc|eut2_east|DLC - Going East!" + dependencies[9]: "dlc|eut2_fr|DLC - Vive la France !" + dependencies[10]: "dlc|eut2_it|DLC - Italia" + dependencies[11]: "rdlc|eut2_metallics|DLC - Metallic Paint Jobs" + dependencies[12]: "dlc|eut2_north|DLC - Scandinavia" + dependencies[13]: "rdlc|eut2_rocket_league|DLC - Rocket League" +} +} +` + +func TestParseUnit(t *testing.T) { + unit, err := parseSIIPlainFile(strings.NewReader(testSii)) + if err != nil { + t.Fatalf("parseSIIPlainFile caused an error: %s", err) + } + + if len(unit.Entries) != 2 { + t.Errorf("Expected 1 block, got %d", len(unit.Entries)) + } + + t.Logf("%#v", unit) + t.Logf("%#v", unit.Entries[0]) + t.Logf("%#v", unit.Entries[1]) +} diff --git a/registry.go b/registry.go new file mode 100644 index 0000000..b6527de --- /dev/null +++ b/registry.go @@ -0,0 +1,33 @@ +package sii + +import ( + "reflect" + "sync" +) + +var ( + blockClass = map[string]reflect.Type{} + blockClassLock = new(sync.RWMutex) + defaultBlockType = reflect.TypeOf(RawBlock{}) +) + +func RegisterBlock(b Block) { + blockClassLock.Lock() + defer blockClassLock.Unlock() + + blockClass[b.Class()] = reflect.TypeOf(b).Elem() +} + +func getBlockInstance(t string) Block { + blockClassLock.RLock() + defer blockClassLock.RUnlock() + + if rt, ok := blockClass[t]; ok { + v := reflect.New(rt).Interface() + if b, ok := v.(Block); ok { + return b + } + } + + return reflect.New(defaultBlockType).Interface().(Block) +} diff --git a/sii.go b/sii.go index 241201a..6aa4b41 100644 --- a/sii.go +++ b/sii.go @@ -121,16 +121,11 @@ func WriteUnitFile(filename string, encrypt bool, data *Unit) error { return errors.New("Not implemented") } -func parseSIIPlainFile(r io.Reader) (*Unit, error) { - // FIXME: Implement this - return nil, errors.New("Not implemented") -} - func readFTHeader(f io.ReaderAt) ([]byte, error) { var ftHeader = make([]byte, 4) if n, err := f.ReadAt(ftHeader, 0); err != nil || n != 4 { if err != nil { - err = errors.Errorf("Received %d / 4 byte header") + err = errors.Errorf("Received %d / 4 byte header", n) } return nil, errors.Wrap(err, "Unable to read 4-byte file header") } diff --git a/unit.go b/unit.go index 6f3e12d..d5cbfca 100644 --- a/unit.go +++ b/unit.go @@ -1,8 +1,9 @@ package sii type Block interface { + Class() string + Init(class, name string) Name() string - Type() string } type Marshaler interface { @@ -16,14 +17,18 @@ type Unmarshaler interface { type RawBlock struct { Data []byte - blockName string - blockType string + blockName string + blockClass string } +func (r *RawBlock) Init(class, name string) { + r.blockClass = class + r.blockName = name +} func (r RawBlock) MarshalSII() []byte { return r.Data } func (r RawBlock) Name() string { return r.blockName } -func (r RawBlock) Type() string { return r.blockType } -func (r *RawBlock) UnmarshalSII(blockName, blockType string, in []byte) error { +func (r RawBlock) Class() string { return r.blockClass } +func (r *RawBlock) UnmarshalSII(in []byte) error { r.Data = in return nil }