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) } }