file.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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.
  69. const MaxDecompressed = 512 << 20
  70. // Read decompresses and decodes a project file.
  71. func Read(r io.Reader) (*File, error) {
  72. zr, err := gzip.NewReader(r)
  73. if err != nil {
  74. return nil, fmt.Errorf("xpj: not a gzip stream: %w", err)
  75. }
  76. defer zr.Close()
  77. raw, err := io.ReadAll(io.LimitReader(zr, MaxDecompressed+1))
  78. if err != nil {
  79. return nil, fmt.Errorf("xpj: decompress: %w", err)
  80. }
  81. if len(raw) > MaxDecompressed {
  82. return nil, fmt.Errorf("xpj: project is larger than %d MB when decompressed", MaxDecompressed>>20)
  83. }
  84. return Decode(raw)
  85. }
  86. // Write encodes f and writes it gzip-compressed, with the gzip header the
  87. // device and the reference writer use: no name, modification time 0, OS Unix.
  88. func (f *File) Write(w io.Writer) error {
  89. raw, err := f.Encode()
  90. if err != nil {
  91. return err
  92. }
  93. zw, err := gzip.NewWriterLevel(w, 6)
  94. if err != nil {
  95. return err
  96. }
  97. zw.OS = 3
  98. if _, err := zw.Write(raw); err != nil {
  99. return err
  100. }
  101. return zw.Close()
  102. }
  103. // ReadFile reads a project file from disk.
  104. func ReadFile(path string) (*File, error) {
  105. fh, err := os.Open(path)
  106. if err != nil {
  107. return nil, err
  108. }
  109. defer fh.Close()
  110. return Read(fh)
  111. }
  112. // WriteFile writes a project file to disk.
  113. func (f *File) WriteFile(path string) error {
  114. var b bytes.Buffer
  115. if err := f.Write(&b); err != nil {
  116. return err
  117. }
  118. return os.WriteFile(path, b.Bytes(), 0o644)
  119. }
  120. // Data returns the "data" object that holds the whole project.
  121. func (f *File) Data() (*Node, error) {
  122. d := f.Root.Lookup("data")
  123. if d == nil || d.Kind != Object {
  124. return nil, errors.New("xpj: no data object")
  125. }
  126. return d, nil
  127. }
  128. // Clone returns a deep copy of f.
  129. func (f *File) Clone() *File {
  130. return &File{Header: append([]string(nil), f.Header...), Root: f.Root.Clone()}
  131. }