diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 248 |
1 files changed, 248 insertions, 0 deletions
@@ -0,0 +1,248 @@ +package main + +import ( + "bufio" + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "slices" + "unicode" + + "github.com/ulikunitz/xz/lzma" +) + +type stateFn func([]byte, bool) (int, []byte, error, stateFn) + +func outsideString(pred func(byte) stateFn) stateFn { + return func(data []byte, atEOF bool) (int, []byte, error, stateFn) { + i := 0 + for ; i < len(data); i++ { + b := data[i] + if unicode.IsSpace(rune(b)) { + continue + } + if f := pred(b); f != nil { + return i + 1, data[i:i+1], nil, f + } + if b == '\000' && atEOF { + return i, nil, bufio.ErrFinalToken, nil + } + return i, nil, fmt.Errorf("unexpected %q", b), nil + } + return i, nil, nil, outsideString(pred) + } +} + +func scanEntsTo(w io.Writer, r io.Reader) error { + w.Write([]byte{'['}) + defer w.Write([]byte{'\n', ']', '\n'}) + // should technically be 1021 as valve's implementation stores "" & \0 + const VMAXLEN = 1024 + + var candidate map[string]string + var key *string + + var topLevel, inEnt, inString stateFn + + topLevel = outsideString(func(c byte) stateFn { + if c == '{' { + candidate = map[string]string{} + return inEnt + } + return nil + }) + + nReported := 0 + inEnt = outsideString(func(c byte) stateFn { + switch c { + case '"': + return inString + case '}': + if nReported > 0 { + w.Write([]byte{','}) + } + w.Write([]byte{'\n', '\t'}) + b, err := json.MarshalIndent(candidate, "\t", "\t") + if err != nil { + die(err) + } + _, err = w.Write(b) + if err != nil { + die(err) + } + nReported++ + candidate = nil + return topLevel + } + return nil + }) + + // "runs" of unescaped characters. we may reread, so pains are taken to + // avoid eager copying. in the overwhelmingly common case of no escapes, + // `runBuf` is entirely bypassed. + runBuf := make([][]byte, VMAXLEN/2) + nt := [2]byte{'\n', '\t'} + inString = func(data []byte, atEOF bool) (int, []byte, error, stateFn) { + var i, iRun, runStart int + inEscape := false + for ; i < len(data); i++ { + if b := data[i]; !inEscape { + switch b { + default: + continue + case '"': + var str string + if iRun == 0 { + str = string(data[:i]) + } else { + runBuf[iRun] = data[runStart:i] + str = string(slices.Concat(runBuf[:iRun+1]...)) + } + + if key == nil { + key = &str + } else { + candidate[*key] = str + key = nil + } + return i + 1, data[:i], nil, inEnt + case '\\': + runBuf[iRun] = data[runStart:i] + iRun++ + inEscape = true + } + } else { + inEscape = false + runStart = i + switch b { + case 'n': + runBuf[iRun] = nt[:1] + case 't': + runBuf[iRun] = nt[1:] + case '\\', '"': + continue + default: + return i, nil, fmt.Errorf("invalid escape %q", b), nil + } + iRun++ + runStart++ + } + } + + if len(data) >= VMAXLEN { + return 0, nil, errors.New("string too long"), nil + } + if atEOF { + return 0, nil, errors.New("truncated entity lump"), nil + } + return 0, nil, nil, inString + } + + scanner := bufio.NewScanner(r) + curState := topLevel + scanner.Split(func(data []byte, eof bool) (adv int, tok []byte, err error) { + adv, tok, err, curState = curState(data, eof) + return + }) + scanner.Buffer(make([]byte, VMAXLEN), VMAXLEN) + for scanner.Scan() { + } + + if err := scanner.Err() + err != nil && !errors.Is(err, bufio.ErrFinalToken) { + return err + } + if candidate != nil || key != nil { + return errors.New("truncated entity lump") + } + return nil +} + +func die(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} + +func readerForBSP(f io.ReaderAt) (r io.Reader, err error) { + var fileofs, filelen int64 + + { + var order binary.ByteOrder + + { + sigbuf := [4]byte{} + if _, err := f.ReadAt(sigbuf[:], 0); err != nil { + return nil, fmt.Errorf("header: %w", err) + } + + switch 0 { + case bytes.Compare(sigbuf[:], []byte("VBSP")): + order = binary.LittleEndian + case bytes.Compare(sigbuf[:], []byte("PSBV")): + order = binary.BigEndian + default: + return nil, errors.New("header: no VBSP signature") + } + } + + lumpbuf := [16]byte{} + if _, err := f.ReadAt(lumpbuf[:], 8); err != nil { + return nil, fmt.Errorf("header: %w", err) + } + // double conversion so reads fail if these go negative + fileofs = int64(int32(order.Uint32(lumpbuf[:4]))) + filelen = int64(int32(order.Uint32(lumpbuf[4:8]))) + } + + lzmabuf := [17]byte{} + if _, err := f.ReadAt(lzmabuf[:], fileofs); err != nil { + return nil, fmt.Errorf("lump contents: %w", err) + } + + // always little endian apparently. + // https://developer.valvesoftware.com/wiki/BSP_(Source)#Lump_compression + if bytes.Compare(lzmabuf[0:4], []byte("LZMA")) == 0 { + actualSize := binary.LittleEndian.Uint32(lzmabuf[4:8]) + lzmaSize := int64(binary.LittleEndian.Uint32(lzmabuf[8:12])) + if lzmaSize+17 != filelen { + return nil, errors.New("filelen and lzmaSize disagree") + } + fakeHeader := [13]byte{} + copy(fakeHeader[0:5], lzmabuf[12:]) + binary.LittleEndian.PutUint64(fakeHeader[5:], uint64(actualSize)) + lzmaReader := io.MultiReader( + bytes.NewReader(fakeHeader[:]), + io.NewSectionReader(f, fileofs+17, lzmaSize), + ) + r, err := lzma.NewReader(lzmaReader) + if err != nil { + return nil, fmt.Errorf("compressed lump: %w", err) + } + return r, nil + } + return io.NewSectionReader(f, fileofs, filelen), nil +} + +func main() { + f, err := os.Open(os.Args[1]) + if err != nil { + die(err) + } + + r, err := readerForBSP(f) + if err != nil { + die(err) + } + + bw := bufio.NewWriter(os.Stdout) + defer bw.Flush() + err = scanEntsTo(bw, r) + + if err != nil { + die(err) + } +} |