1
0

file.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. package xpj
  2. import (
  3. "bytes"
  4. "compress/gzip"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "os"
  9. "strings"
  10. )
  11. // HeaderLines is the number of text lines before the JSON body.
  12. const HeaderLines = 5
  13. // Header values seen in every file saved by firmware 1.3.0.12.
  14. var TestedHeader = []string{"ACVS", "1.3.0.12", "SerialisableProjectData", "json", "Linux"}
  15. // File is a decoded project file.
  16. type File struct {
  17. Header []string // the five header lines, without newlines
  18. Root *Node // the JSON document, {"data": {...}}
  19. }
  20. // Decode parses a decompressed project file.
  21. func Decode(raw []byte) (*File, error) {
  22. parts := bytes.SplitN(raw, []byte("\n"), HeaderLines+1)
  23. if len(parts) != HeaderLines+1 {
  24. return nil, errors.New("xpj: file is shorter than its five header lines")
  25. }
  26. header := make([]string, HeaderLines)
  27. for i := range header {
  28. header[i] = string(parts[i])
  29. }
  30. if header[0] != "ACVS" {
  31. return nil, fmt.Errorf("xpj: not an MPC project (first line %q, want ACVS)", header[0])
  32. }
  33. root, err := ParseJSON(parts[HeaderLines])
  34. if err != nil {
  35. return nil, err
  36. }
  37. if root.Kind != Object {
  38. return nil, fmt.Errorf("xpj: body is %s, want object", root.Kind)
  39. }
  40. return &File{Header: header, Root: root}, nil
  41. }
  42. // Encode returns the decompressed file: header lines, then the JSON body.
  43. func (f *File) Encode() ([]byte, error) {
  44. if len(f.Header) != HeaderLines {
  45. return nil, fmt.Errorf("xpj: header has %d lines, want %d", len(f.Header), HeaderLines)
  46. }
  47. for _, h := range f.Header {
  48. if strings.ContainsAny(h, "\r\n") {
  49. return nil, fmt.Errorf("xpj: header line %q contains a newline", h)
  50. }
  51. }
  52. body, err := f.Root.MarshalJSON()
  53. if err != nil {
  54. return nil, err
  55. }
  56. var b bytes.Buffer
  57. b.Grow(len(body) + 64)
  58. for _, h := range f.Header {
  59. b.WriteString(h)
  60. b.WriteByte('\n')
  61. }
  62. b.Write(body)
  63. return b.Bytes(), nil
  64. }
  65. // MaxDecompressed is the largest project Read accepts, in decompressed
  66. // bytes. A small file can decompress to gigabytes (a 1 MB file padded with
  67. // spaces used 3.1 GB), so Read stops there instead of running out of
  68. // memory. Device projects are about 3 MB; 256 MB holds about half a
  69. // million recorded events.
  70. const MaxDecompressed = 256 << 20
  71. // Read decompresses and decodes a project file.
  72. func Read(r io.Reader) (*File, error) {
  73. zr, err := gzip.NewReader(r)
  74. if err != nil {
  75. return nil, fmt.Errorf("xpj: not a gzip stream: %w", err)
  76. }
  77. defer zr.Close()
  78. raw, err := io.ReadAll(io.LimitReader(zr, MaxDecompressed+1))
  79. if err != nil {
  80. return nil, fmt.Errorf("xpj: decompress: %w", err)
  81. }
  82. if len(raw) > MaxDecompressed {
  83. return nil, fmt.Errorf("xpj: project is larger than %d MB when decompressed", MaxDecompressed>>20)
  84. }
  85. return Decode(raw)
  86. }
  87. // Write encodes f and writes it gzip-compressed, with the gzip header the
  88. // device and the reference writer use: no name, modification time 0, OS Unix.
  89. func (f *File) Write(w io.Writer) error {
  90. raw, err := f.Encode()
  91. if err != nil {
  92. return err
  93. }
  94. zw, err := gzip.NewWriterLevel(w, 6)
  95. if err != nil {
  96. return err
  97. }
  98. zw.OS = 3
  99. if _, err := zw.Write(raw); err != nil {
  100. return err
  101. }
  102. return zw.Close()
  103. }
  104. // ReadFile reads a project file from disk.
  105. func ReadFile(path string) (*File, error) {
  106. fh, err := os.Open(path)
  107. if err != nil {
  108. return nil, err
  109. }
  110. defer fh.Close()
  111. return Read(fh)
  112. }
  113. // WriteFile writes a project file to disk.
  114. func (f *File) WriteFile(path string) error {
  115. var b bytes.Buffer
  116. if err := f.Write(&b); err != nil {
  117. return err
  118. }
  119. return os.WriteFile(path, b.Bytes(), 0o644)
  120. }
  121. // Data returns the "data" object that holds the whole project.
  122. func (f *File) Data() (*Node, error) {
  123. d := f.Root.Lookup("data")
  124. if d == nil || d.Kind != Object {
  125. return nil, errors.New("xpj: no data object")
  126. }
  127. return d, nil
  128. }
  129. // Clone returns a deep copy of f.
  130. func (f *File) Clone() *File {
  131. return &File{Header: append([]string(nil), f.Header...), Root: f.Root.Clone()}
  132. }