Web-based Winamp controller for CarPC � Go backend, mobile-first UI
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

224 行
7.0KB

  1. package rating
  2. import (
  3. "encoding/binary"
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. )
  9. // errPatchRefused signals that the in-place patcher cannot (or should not)
  10. // apply the change. The caller falls back to a full rewrite. Real I/O errors
  11. // are returned as-is so we don't silently retry corrupt-disk situations.
  12. var errPatchRefused = errors.New("rating: in-place patch refused")
  13. // popmEmail is the identifier we write into new POPM frames (Winamp convention).
  14. const popmEmail = "rating@winamp.com"
  15. // popmCounterBytes — how many bytes of zero "play counter" we append after the
  16. // rating byte when inserting a fresh frame. 0 is spec-legal but some readers
  17. // (notably old WMP builds) expect ≥4. 4 zero bytes costs us nothing and is
  18. // what bogem/id3v2 emits by default for a fresh frame.
  19. const popmCounterBytes = 4
  20. // tryPatchPOPM attempts to update (or insert) the POPM rating with the
  21. // smallest possible write — ideally one byte. It never grows the tag area:
  22. // when there is no existing POPM and no room in padding it refuses
  23. // (returns errPatchRefused) so the caller can fall back to a full rewrite.
  24. //
  25. // stars=0 with no existing POPM is a no-op success (file already unrated).
  26. // stars=0 with an existing POPM rewrites the rating byte to 0 (the spec's
  27. // "unknown" value); the frame remains but Get() returns 0.
  28. //
  29. // Safety invariants enforced:
  30. // - Only ID3v2.3 / v2.4 with no header-level flags (no unsync, no extended
  31. // header, no footer, no experimental).
  32. // - All writes must land strictly inside the tag area
  33. // (offset ∈ [10, 10+tagSize)).
  34. // - Padding region must verify as all-zero before we write into it.
  35. // - POPM frame flags must be zero (no compression / encryption / data-length
  36. // indicator that would change how we'd interpret the rating offset).
  37. // - Frame walking respects each frame's declared size; any inconsistency
  38. // (size out of bounds, invalid frame ID) aborts via errPatchRefused.
  39. func tryPatchPOPM(path string, stars int) error {
  40. if stars < 0 || stars > 5 {
  41. return fmt.Errorf("tryPatchPOPM: stars out of range: %d", stars)
  42. }
  43. f, err := os.OpenFile(path, os.O_RDWR, 0)
  44. if err != nil {
  45. return err
  46. }
  47. defer f.Close()
  48. // ── Parse 10-byte ID3v2 header ────────────────────────────────────────────
  49. var hdr [10]byte
  50. if _, err := io.ReadFull(f, hdr[:]); err != nil {
  51. return errPatchRefused
  52. }
  53. if string(hdr[:3]) != "ID3" {
  54. return errPatchRefused
  55. }
  56. ver := hdr[3]
  57. if ver != 3 && ver != 4 {
  58. return errPatchRefused
  59. }
  60. // Any header-level flag bit set → refuse. Includes unsynchronisation,
  61. // extended header, experimental, and (v2.4) footer present.
  62. if hdr[5] != 0 {
  63. return errPatchRefused
  64. }
  65. // Synchsafe size: each of the 4 bytes must have its high bit clear.
  66. if hdr[6]&0x80 != 0 || hdr[7]&0x80 != 0 || hdr[8]&0x80 != 0 || hdr[9]&0x80 != 0 {
  67. return errPatchRefused
  68. }
  69. tagSize := int(uint32(hdr[6])<<21 | uint32(hdr[7])<<14 | uint32(hdr[8])<<7 | uint32(hdr[9]))
  70. if tagSize <= 0 || tagSize > 64*1024*1024 {
  71. return errPatchRefused
  72. }
  73. body := make([]byte, tagSize)
  74. if _, err := io.ReadFull(f, body); err != nil {
  75. return errPatchRefused
  76. }
  77. // ── Walk frames, look for existing POPM ───────────────────────────────────
  78. off := 0
  79. for off+10 <= tagSize {
  80. // First byte zero where a frame ID would start → padding region begins.
  81. if body[off] == 0 {
  82. break
  83. }
  84. id := string(body[off : off+4])
  85. if !validFrameID(id) {
  86. return errPatchRefused
  87. }
  88. var frameSize int
  89. switch ver {
  90. case 3:
  91. frameSize = int(binary.BigEndian.Uint32(body[off+4 : off+8]))
  92. case 4:
  93. b := body[off+4 : off+8]
  94. if b[0]&0x80 != 0 || b[1]&0x80 != 0 || b[2]&0x80 != 0 || b[3]&0x80 != 0 {
  95. return errPatchRefused
  96. }
  97. frameSize = int(uint32(b[0])<<21 | uint32(b[1])<<14 | uint32(b[2])<<7 | uint32(b[3]))
  98. }
  99. if frameSize <= 0 || off+10+frameSize > tagSize {
  100. return errPatchRefused
  101. }
  102. if id == "POPM" {
  103. // Reject if frame has any flags — they could change data layout
  104. // (compression, encryption, v2.4 data-length indicator).
  105. if body[off+8] != 0 || body[off+9] != 0 {
  106. return errPatchRefused
  107. }
  108. data := body[off+10 : off+10+frameSize]
  109. // Find the null terminator of the email field.
  110. nullIdx := -1
  111. for i, c := range data {
  112. if c == 0 {
  113. nullIdx = i
  114. break
  115. }
  116. }
  117. if nullIdx < 0 || nullIdx+1 >= len(data) {
  118. return errPatchRefused
  119. }
  120. ratingFileOffset := int64(10 + off + 10 + nullIdx + 1)
  121. // Strict bounds check: must be strictly inside the tag region.
  122. if ratingFileOffset < 10 || ratingFileOffset >= int64(10+tagSize) {
  123. return errPatchRefused
  124. }
  125. newRating := starsToPOPM(stars)
  126. if _, err := f.WriteAt([]byte{newRating}, ratingFileOffset); err != nil {
  127. return err
  128. }
  129. return f.Sync()
  130. }
  131. off += 10 + frameSize
  132. }
  133. // ── No POPM frame in the tag ──────────────────────────────────────────────
  134. if stars == 0 {
  135. return nil // already unrated, nothing to write
  136. }
  137. padStart := off
  138. // Verify the entire remaining tag region is zero. If anything else is
  139. // hiding there (some other tool's data, a malformed frame), refuse.
  140. for i := padStart; i < tagSize; i++ {
  141. if body[i] != 0 {
  142. return errPatchRefused
  143. }
  144. }
  145. paddingLen := tagSize - padStart
  146. frame := buildPOPMFrame(ver, stars)
  147. if len(frame) > paddingLen {
  148. return errPatchRefused
  149. }
  150. writeOffset := int64(10 + padStart)
  151. // Strict bounds: write must end at or before the end of the tag region.
  152. if writeOffset < 10 || writeOffset+int64(len(frame)) > int64(10+tagSize) {
  153. return errPatchRefused
  154. }
  155. if _, err := f.WriteAt(frame, writeOffset); err != nil {
  156. return err
  157. }
  158. return f.Sync()
  159. }
  160. // validFrameID returns true if id is a 4-character ASCII frame identifier
  161. // using only [A-Z0-9], per ID3v2.3/2.4. Anything else and we're not looking
  162. // at a real frame.
  163. func validFrameID(id string) bool {
  164. if len(id) != 4 {
  165. return false
  166. }
  167. for i := 0; i < 4; i++ {
  168. c := id[i]
  169. if !((c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
  170. return false
  171. }
  172. }
  173. return true
  174. }
  175. // buildPOPMFrame returns the full bytes of a fresh POPM frame:
  176. //
  177. // "POPM" | size(4) | flags(2) | email | 0x00 | rating | counter(4 zero bytes)
  178. //
  179. // Size encoding depends on the ID3v2 minor version (3 = plain BE uint32,
  180. // 4 = synchsafe).
  181. func buildPOPMFrame(ver byte, stars int) []byte {
  182. dataLen := len(popmEmail) + 1 + 1 + popmCounterBytes
  183. out := make([]byte, 10+dataLen)
  184. copy(out[0:4], "POPM")
  185. switch ver {
  186. case 3:
  187. binary.BigEndian.PutUint32(out[4:8], uint32(dataLen))
  188. case 4:
  189. s := uint32(dataLen)
  190. out[4] = byte((s >> 21) & 0x7F)
  191. out[5] = byte((s >> 14) & 0x7F)
  192. out[6] = byte((s >> 7) & 0x7F)
  193. out[7] = byte(s & 0x7F)
  194. }
  195. // flags out[8], out[9] are zero
  196. copy(out[10:], popmEmail)
  197. out[10+len(popmEmail)] = 0 // null terminator
  198. out[10+len(popmEmail)+1] = starsToPOPM(stars)
  199. // counter bytes already zero
  200. return out
  201. }