Web-based Winamp controller for CarPC � Go backend, mobile-first UI
25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

313 lines
9.0KB

  1. package rating
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "os"
  6. "testing"
  7. )
  8. // buildTaggedMP3 builds a synthetic MP3 with a real ID3v2 tag.
  9. // - ver: 3 or 4 (ID3v2 minor version)
  10. // - frames: pre-built frame bytes (full frame including 10-byte frame header)
  11. // - paddingLen: bytes of zero padding inside the tag
  12. // - audio: payload after the tag
  13. //
  14. // The returned bytes are: 10-byte ID3 header | frames | padding | audio.
  15. func buildTaggedMP3(ver byte, frames []byte, paddingLen int, audio []byte) []byte {
  16. tagSize := len(frames) + paddingLen
  17. var hdr [10]byte
  18. hdr[0], hdr[1], hdr[2] = 'I', 'D', '3'
  19. hdr[3] = ver
  20. hdr[4] = 0
  21. hdr[5] = 0 // no flags
  22. s := uint32(tagSize)
  23. hdr[6] = byte((s >> 21) & 0x7F)
  24. hdr[7] = byte((s >> 14) & 0x7F)
  25. hdr[8] = byte((s >> 7) & 0x7F)
  26. hdr[9] = byte(s & 0x7F)
  27. out := make([]byte, 0, 10+tagSize+len(audio))
  28. out = append(out, hdr[:]...)
  29. out = append(out, frames...)
  30. out = append(out, make([]byte, paddingLen)...)
  31. out = append(out, audio...)
  32. return out
  33. }
  34. // makePOPMFrame builds a POPM frame matching ver's size encoding.
  35. func makePOPMFrame(ver byte, email string, rating byte, counter []byte) []byte {
  36. dataLen := len(email) + 1 + 1 + len(counter)
  37. out := make([]byte, 10+dataLen)
  38. copy(out[0:4], "POPM")
  39. switch ver {
  40. case 3:
  41. binary.BigEndian.PutUint32(out[4:8], uint32(dataLen))
  42. case 4:
  43. s := uint32(dataLen)
  44. out[4] = byte((s >> 21) & 0x7F)
  45. out[5] = byte((s >> 14) & 0x7F)
  46. out[6] = byte((s >> 7) & 0x7F)
  47. out[7] = byte(s & 0x7F)
  48. }
  49. copy(out[10:], email)
  50. out[10+len(email)] = 0
  51. out[10+len(email)+1] = rating
  52. copy(out[10+len(email)+2:], counter)
  53. return out
  54. }
  55. func fakeAudio(n int) []byte {
  56. a := make([]byte, n)
  57. for i := range a {
  58. a[i] = byte((i * 7) & 0xFF)
  59. }
  60. return a
  61. }
  62. // --- TESTS: patch path with existing POPM (1-byte write) ---------------------
  63. func TestPatch_existingPOPM_v23(t *testing.T) {
  64. for _, stars := range []int{1, 2, 3, 4, 5, 0} {
  65. t.Run("", func(t *testing.T) {
  66. // Use a starting rating (200) that doesn't match any starsToPOPM value
  67. // so we always observe a 1-byte change regardless of target stars.
  68. frame := makePOPMFrame(3, "rating@winamp.com", 200, []byte{0, 0, 0, 0})
  69. audio := fakeAudio(2048)
  70. data := buildTaggedMP3(3, frame, 512, audio)
  71. path := writeTempMP3(t, data)
  72. before, _ := os.ReadFile(path)
  73. if err := tryPatchPOPM(path, stars); err != nil {
  74. t.Fatalf("tryPatchPOPM: %v", err)
  75. }
  76. after, _ := os.ReadFile(path)
  77. if len(before) != len(after) {
  78. t.Fatalf("file size changed: %d → %d", len(before), len(after))
  79. }
  80. // Exactly ONE byte should differ — the rating byte.
  81. diffCount := 0
  82. diffOff := -1
  83. for i := range before {
  84. if before[i] != after[i] {
  85. diffCount++
  86. diffOff = i
  87. }
  88. }
  89. if diffCount != 1 {
  90. t.Fatalf("expected exactly 1 byte changed, got %d", diffCount)
  91. }
  92. // The rating byte sits at: 10 (header) + 10 (frame hdr) + len(email)+1
  93. wantOff := 10 + 10 + len("rating@winamp.com") + 1
  94. if diffOff != wantOff {
  95. t.Errorf("changed byte at %d, expected at %d", diffOff, wantOff)
  96. }
  97. if after[diffOff] != starsToPOPM(stars) {
  98. t.Errorf("rating byte = %d, want %d", after[diffOff], starsToPOPM(stars))
  99. }
  100. // Audio region untouched.
  101. audioStart := 10 + 10 + 23 + 512 // hdr + frame + padding
  102. if !bytes.Equal(after[audioStart:], audio) {
  103. t.Error("audio data corrupted")
  104. }
  105. // Get() should reflect the new rating
  106. got, _ := Get(path)
  107. if got != stars {
  108. t.Errorf("Get() = %d, want %d", got, stars)
  109. }
  110. })
  111. }
  112. }
  113. func TestPatch_existingPOPM_v24(t *testing.T) {
  114. frame := makePOPMFrame(4, "rating@winamp.com", 1, []byte{0, 0, 0, 0})
  115. audio := fakeAudio(1024)
  116. data := buildTaggedMP3(4, frame, 256, audio)
  117. path := writeTempMP3(t, data)
  118. if err := tryPatchPOPM(path, 5); err != nil {
  119. t.Fatalf("tryPatchPOPM: %v", err)
  120. }
  121. got, _ := Get(path)
  122. if got != 5 {
  123. t.Errorf("Get() = %d, want 5", got)
  124. }
  125. }
  126. // --- TESTS: patch path inserts new POPM into padding -------------------------
  127. func TestPatch_insertIntoPadding(t *testing.T) {
  128. audio := fakeAudio(1024)
  129. data := buildTaggedMP3(3, nil, 1024, audio) // empty tag body, all padding
  130. path := writeTempMP3(t, data)
  131. beforeSize := int64(len(data))
  132. if err := tryPatchPOPM(path, 4); err != nil {
  133. t.Fatalf("tryPatchPOPM: %v", err)
  134. }
  135. st, _ := os.Stat(path)
  136. if st.Size() != beforeSize {
  137. t.Fatalf("file size changed: %d → %d", beforeSize, st.Size())
  138. }
  139. got, err := Get(path)
  140. if err != nil {
  141. t.Fatalf("Get: %v", err)
  142. }
  143. if got != 4 {
  144. t.Errorf("Get() = %d, want 4", got)
  145. }
  146. // Audio untouched.
  147. after, _ := os.ReadFile(path)
  148. audioStart := 10 + 1024
  149. if !bytes.Equal(after[audioStart:], audio) {
  150. t.Error("audio data corrupted")
  151. }
  152. }
  153. // --- TESTS: patch path refuses safely on unsupported inputs ------------------
  154. func TestPatch_refusesNoID3(t *testing.T) {
  155. path := writeTempMP3(t, fakeAudio(2048))
  156. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  157. t.Errorf("got %v, want errPatchRefused", err)
  158. }
  159. }
  160. func TestPatch_refusesUnsyncFlag(t *testing.T) {
  161. data := buildTaggedMP3(3, nil, 1024, fakeAudio(512))
  162. data[5] = 0x80 // set unsync flag
  163. path := writeTempMP3(t, data)
  164. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  165. t.Errorf("got %v, want errPatchRefused", err)
  166. }
  167. }
  168. func TestPatch_refusesUnknownVersion(t *testing.T) {
  169. data := buildTaggedMP3(2, nil, 1024, fakeAudio(512))
  170. path := writeTempMP3(t, data)
  171. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  172. t.Errorf("got %v, want errPatchRefused", err)
  173. }
  174. }
  175. func TestPatch_refusesPaddingTooSmall(t *testing.T) {
  176. // No POPM, only 5 bytes padding — new frame needs ~33 bytes.
  177. data := buildTaggedMP3(3, nil, 5, fakeAudio(512))
  178. path := writeTempMP3(t, data)
  179. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  180. t.Errorf("got %v, want errPatchRefused", err)
  181. }
  182. }
  183. func TestPatch_refusesPOPMWithFlags(t *testing.T) {
  184. frame := makePOPMFrame(3, "rating@winamp.com", 128, []byte{0, 0, 0, 0})
  185. frame[9] = 0x01 // set a frame flag
  186. data := buildTaggedMP3(3, frame, 256, fakeAudio(512))
  187. path := writeTempMP3(t, data)
  188. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  189. t.Errorf("got %v, want errPatchRefused", err)
  190. }
  191. }
  192. func TestPatch_refusesInvalidFrameID(t *testing.T) {
  193. // Garbage frame ID (lowercase invalid).
  194. bad := []byte{'a', 'b', 'c', 'd', 0, 0, 0, 4, 0, 0, 0, 0, 0, 0}
  195. data := buildTaggedMP3(3, bad, 256, fakeAudio(512))
  196. path := writeTempMP3(t, data)
  197. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  198. t.Errorf("got %v, want errPatchRefused", err)
  199. }
  200. }
  201. func TestPatch_refusesPOPMOversizedSize(t *testing.T) {
  202. // POPM with size larger than what fits in the tag — must refuse, not panic.
  203. frame := makePOPMFrame(3, "rating@winamp.com", 128, []byte{0, 0, 0, 0})
  204. binary.BigEndian.PutUint32(frame[4:8], 999999) // lie about size
  205. data := buildTaggedMP3(3, frame, 256, fakeAudio(512))
  206. path := writeTempMP3(t, data)
  207. if err := tryPatchPOPM(path, 3); err != errPatchRefused {
  208. t.Errorf("got %v, want errPatchRefused", err)
  209. }
  210. }
  211. // --- TESTS: stars=0 semantics in patch path ----------------------------------
  212. func TestPatch_starsZeroWithNoFrame_noWrite(t *testing.T) {
  213. data := buildTaggedMP3(3, nil, 1024, fakeAudio(512))
  214. path := writeTempMP3(t, data)
  215. before, _ := os.ReadFile(path)
  216. if err := tryPatchPOPM(path, 0); err != nil {
  217. t.Fatalf("unexpected: %v", err)
  218. }
  219. after, _ := os.ReadFile(path)
  220. if !bytes.Equal(before, after) {
  221. t.Error("file changed even though stars=0 and no existing frame")
  222. }
  223. }
  224. // --- TESTS: walks past non-POPM frames correctly ----------------------------
  225. func TestPatch_skipsOtherFrames(t *testing.T) {
  226. // TIT2 (title) frame first, then POPM. Encoding byte 0x00 = ISO-8859-1.
  227. tit2Data := append([]byte{0x00}, "Test Title"...)
  228. tit2 := make([]byte, 10+len(tit2Data))
  229. copy(tit2[0:4], "TIT2")
  230. binary.BigEndian.PutUint32(tit2[4:8], uint32(len(tit2Data)))
  231. copy(tit2[10:], tit2Data)
  232. popm := makePOPMFrame(3, "rating@winamp.com", 1, []byte{0, 0, 0, 0})
  233. frames := append(tit2, popm...)
  234. audio := fakeAudio(1024)
  235. data := buildTaggedMP3(3, frames, 512, audio)
  236. path := writeTempMP3(t, data)
  237. if err := tryPatchPOPM(path, 5); err != nil {
  238. t.Fatalf("tryPatchPOPM: %v", err)
  239. }
  240. got, _ := Get(path)
  241. if got != 5 {
  242. t.Errorf("Get() = %d, want 5", got)
  243. }
  244. // Audio untouched.
  245. after, _ := os.ReadFile(path)
  246. audioStart := 10 + len(frames) + 512
  247. if !bytes.Equal(after[audioStart:], audio) {
  248. t.Error("audio data corrupted")
  249. }
  250. }
  251. // --- TESTS: Set() integration uses patch when it can ------------------------
  252. func TestSet_usesPatchPathWhenPossible(t *testing.T) {
  253. // File has padding → patch should succeed → audio MUST remain byte-identical.
  254. audio := fakeAudio(4096)
  255. data := buildTaggedMP3(3, nil, 2048, audio)
  256. path := writeTempMP3(t, data)
  257. beforeSize := int64(len(data))
  258. if err := Set(path, 3); err != nil {
  259. t.Fatalf("Set: %v", err)
  260. }
  261. st, _ := os.Stat(path)
  262. if st.Size() != beforeSize {
  263. t.Errorf("Set() with available padding changed file size: %d → %d (patch path should be used)",
  264. beforeSize, st.Size())
  265. }
  266. got, _ := Get(path)
  267. if got != 3 {
  268. t.Errorf("Get() = %d, want 3", got)
  269. }
  270. }