finder_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
  2. // SPDX-License-Identifier: Apache-2.0
  3. package icon // import "miniflux.app/v2/internal/reader/icon"
  4. import (
  5. "bytes"
  6. "encoding/base64"
  7. "encoding/binary"
  8. "hash/crc32"
  9. "image"
  10. "strings"
  11. "testing"
  12. "miniflux.app/v2/internal/model"
  13. )
  14. func TestParseImageDataURL(t *testing.T) {
  15. iconURL := "data:image/webp;base64,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"
  16. icon, err := parseImageDataURL(iconURL)
  17. if err != nil {
  18. t.Fatalf(`We should be able to parse valid data URL: %v`, err)
  19. }
  20. if icon.MimeType != "image/webp" {
  21. t.Fatal(`Invalid mime type parsed`)
  22. }
  23. if icon.Hash == "" {
  24. t.Fatal(`Image hash should be computed`)
  25. }
  26. }
  27. func TestParseImageDataURLWithNoEncoding(t *testing.T) {
  28. iconURL := `data:image/webp,%3Ch1%3EHello%2C%20World%21%3C%2Fh1%3E`
  29. icon, err := parseImageDataURL(iconURL)
  30. if err != nil {
  31. t.Fatalf(`We should be able to parse valid data URL: %v`, err)
  32. }
  33. if icon.MimeType != "image/webp" {
  34. t.Fatal(`Invalid mime type parsed`)
  35. }
  36. if string(icon.Content) == "Hello, World!" {
  37. t.Fatal(`Value should be URL-decoded`)
  38. }
  39. if icon.Hash == "" {
  40. t.Fatal(`Image hash should be computed`)
  41. }
  42. }
  43. func TestParseImageWithRawSVGEncodedInUTF8(t *testing.T) {
  44. iconURL := `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 456 456'><circle></circle></svg>`
  45. icon, err := parseImageDataURL(iconURL)
  46. if err != nil {
  47. t.Fatalf(`We should be able to parse valid data URL: %v`, err)
  48. }
  49. if icon.MimeType != "image/svg+xml" {
  50. t.Fatal(`Invalid mime type parsed`)
  51. }
  52. if icon.Hash == "" {
  53. t.Fatal(`Image hash should be computed`)
  54. }
  55. if string(icon.Content) != `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 456 456'><circle></circle></svg>` {
  56. t.Fatal(`Invalid SVG content`)
  57. }
  58. }
  59. func TestParseImageDataURLWithNoMediaTypeAndNoEncoding(t *testing.T) {
  60. iconURL := `data:,Hello%2C%20World%21`
  61. _, err := parseImageDataURL(iconURL)
  62. if err == nil {
  63. t.Fatal(`We should detect invalid mime type`)
  64. }
  65. }
  66. func TestParseInvalidImageDataURLWithBadMimeType(t *testing.T) {
  67. _, err := parseImageDataURL("data:text/plain;base64,blob")
  68. if err == nil {
  69. t.Fatal(`We should detect invalid mime type`)
  70. }
  71. }
  72. func TestParseInvalidImageDataURLWithUnsupportedEncoding(t *testing.T) {
  73. _, err := parseImageDataURL("data:image/png;base32,blob")
  74. if err == nil {
  75. t.Fatal(`We should detect unsupported encoding`)
  76. }
  77. }
  78. func TestParseInvalidImageDataURLWithNoData(t *testing.T) {
  79. _, err := parseImageDataURL("data:image/png;base64,")
  80. if err == nil {
  81. t.Fatal(`We should detect invalid encoded data`)
  82. }
  83. }
  84. func TestParseInvalidImageDataURL(t *testing.T) {
  85. _, err := parseImageDataURL("data:image/jpeg")
  86. if err == nil {
  87. t.Fatal(`We should detect malformed image data URL`)
  88. }
  89. }
  90. func TestParseInvalidImageDataURLWithWrongPrefix(t *testing.T) {
  91. _, err := parseImageDataURL("data,test")
  92. if err == nil {
  93. t.Fatal(`We should detect malformed image data URL`)
  94. }
  95. }
  96. func TestFindIconURLsFromHTMLDocument_MultipleIcons(t *testing.T) {
  97. html := `<!DOCTYPE html>
  98. <html>
  99. <head>
  100. <link rel="icon" href="/favicon.ico">
  101. <link rel="shortcut icon" href="/shortcut-favicon.ico">
  102. <link rel="icon shortcut" href="/icon-shortcut.ico">
  103. <link rel="apple-touch-icon" href="/apple-touch-icon.png">
  104. </head>
  105. </html>`
  106. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org", strings.NewReader(html), "text/html")
  107. if err != nil {
  108. t.Fatal(err)
  109. }
  110. expected := []string{
  111. "https://example.org/favicon.ico",
  112. "https://example.org/shortcut-favicon.ico",
  113. "https://example.org/icon-shortcut.ico",
  114. "https://example.org/apple-touch-icon.png",
  115. }
  116. if len(iconURLs) != len(expected) {
  117. t.Fatalf("Expected %d icon URLs, got %d", len(expected), len(iconURLs))
  118. }
  119. for i, expectedURL := range expected {
  120. if iconURLs[i] != expectedURL {
  121. t.Errorf("Expected icon URL %d to be %q, got %q", i, expectedURL, iconURLs[i])
  122. }
  123. }
  124. }
  125. func TestFindIconURLsFromHTMLDocument_CaseInsensitiveRel(t *testing.T) {
  126. html := `<!DOCTYPE html>
  127. <html>
  128. <head>
  129. <link rel="ICON" href="/favicon1.ico">
  130. <link rel="Icon" href="/favicon2.ico">
  131. <link rel="SHORTCUT ICON" href="/favicon3.ico">
  132. <link rel="Shortcut Icon" href="/favicon4.ico">
  133. <link rel="ICON SHORTCUT" href="/favicon5.ico">
  134. <link rel="Icon Shortcut" href="favicon6.ico">
  135. </head>
  136. </html>`
  137. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org/folder/", strings.NewReader(html), "text/html")
  138. if err != nil {
  139. t.Fatal(err)
  140. }
  141. expected := []string{
  142. "https://example.org/favicon1.ico",
  143. "https://example.org/favicon2.ico",
  144. "https://example.org/favicon3.ico",
  145. "https://example.org/favicon4.ico",
  146. "https://example.org/favicon5.ico",
  147. "https://example.org/folder/favicon6.ico",
  148. }
  149. if len(iconURLs) != len(expected) {
  150. t.Fatalf("Expected %d icon URLs, got %d", len(expected), len(iconURLs))
  151. }
  152. for i, expectedURL := range expected {
  153. if iconURLs[i] != expectedURL {
  154. t.Errorf("Expected icon URL %d to be %q, got %q", i, expectedURL, iconURLs[i])
  155. }
  156. }
  157. }
  158. func TestFindIconURLsFromHTMLDocument_NoIcons(t *testing.T) {
  159. html := `<!DOCTYPE html>
  160. <html>
  161. <head>
  162. <title>No Icons Here</title>
  163. <link rel="stylesheet" href="/style.css">
  164. <link rel="canonical" href="https://example.com">
  165. </head>
  166. </html>`
  167. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org", strings.NewReader(html), "text/html")
  168. if err != nil {
  169. t.Fatal(err)
  170. }
  171. if len(iconURLs) != 0 {
  172. t.Fatalf("Expected 0 icon URLs, got %d: %v", len(iconURLs), iconURLs)
  173. }
  174. }
  175. func TestFindIconURLsFromHTMLDocument_EmptyHref(t *testing.T) {
  176. html := `<!DOCTYPE html>
  177. <html>
  178. <head>
  179. <link rel="icon" href="">
  180. <link rel="icon" href=" ">
  181. <link rel="icon">
  182. <link rel="shortcut icon" href="/valid-icon.ico">
  183. </head>
  184. </html>`
  185. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org", strings.NewReader(html), "text/html")
  186. if err != nil {
  187. t.Fatal(err)
  188. }
  189. expected := []string{"https://example.org/valid-icon.ico"}
  190. if len(iconURLs) != len(expected) {
  191. t.Fatalf("Expected %d icon URLs, got %d", len(expected), len(iconURLs))
  192. }
  193. if iconURLs[0] != expected[0] {
  194. t.Errorf("Expected icon URL to be %q, got %q", expected[0], iconURLs[0])
  195. }
  196. }
  197. func TestFindIconURLsFromHTMLDocument_DataURLs(t *testing.T) {
  198. html := `<!DOCTYPE html>
  199. <html>
  200. <head>
  201. <link rel="icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAGAhGAQ+QAAAABJRU5ErkJggg==">
  202. <link rel="shortcut icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>">
  203. <link rel="icon" href="/regular-icon.ico">
  204. </head>
  205. </html>`
  206. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org/folder", strings.NewReader(html), "text/html")
  207. if err != nil {
  208. t.Fatal(err)
  209. }
  210. expected := []string{
  211. "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChAGAhGAQ+QAAAABJRU5ErkJggg==",
  212. "data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'></svg>",
  213. "https://example.org/regular-icon.ico",
  214. }
  215. if len(iconURLs) != len(expected) {
  216. t.Fatalf("Expected %d icon URLs, got %d", len(expected), len(iconURLs))
  217. }
  218. for i, expectedURL := range expected {
  219. if iconURLs[i] != expectedURL {
  220. t.Errorf("Expected icon URL %d to be %q, got %q", i, expectedURL, iconURLs[i])
  221. }
  222. }
  223. }
  224. func TestFindIconURLsFromHTMLDocument_RelativeAndAbsoluteURLs(t *testing.T) {
  225. html := `<!DOCTYPE html>
  226. <html>
  227. <head>
  228. <link rel="icon" href="/absolute-path.ico">
  229. <link rel="icon" href="relative-path.ico">
  230. <link rel="icon" href="../parent-dir.ico">
  231. <link rel="icon" href="https://example.com/external.ico">
  232. <link rel="icon" href="//cdn.example.com/protocol-relative.ico">
  233. </head>
  234. </html>`
  235. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org/folder/", strings.NewReader(html), "text/html")
  236. if err != nil {
  237. t.Fatal(err)
  238. }
  239. expected := []string{
  240. "https://example.org/absolute-path.ico",
  241. "https://example.org/folder/relative-path.ico",
  242. "https://example.org/parent-dir.ico",
  243. "https://example.com/external.ico",
  244. "https://cdn.example.com/protocol-relative.ico",
  245. }
  246. if len(iconURLs) != len(expected) {
  247. t.Fatalf("Expected %d icon URLs, got %d", len(expected), len(iconURLs))
  248. }
  249. for i, expectedURL := range expected {
  250. if iconURLs[i] != expectedURL {
  251. t.Errorf("Expected icon URL %d to be %q, got %q", i, expectedURL, iconURLs[i])
  252. }
  253. }
  254. }
  255. func TestFindIconURLsFromHTMLDocument_InvalidHTML(t *testing.T) {
  256. html := `<!DOCTYPE html>
  257. <html>
  258. <head>
  259. <link rel="icon" href="/valid-before-error.ico">
  260. <link rel="icon" href="/unclosed-tag.ico"
  261. <link rel="shortcut icon" href="/valid-after-error.ico">
  262. </head>
  263. </html>`
  264. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org", strings.NewReader(html), "text/html")
  265. if err != nil {
  266. t.Fatal(err)
  267. }
  268. // goquery should handle malformed HTML gracefully
  269. if len(iconURLs) == 0 {
  270. t.Fatal("Expected to find some icon URLs even with malformed HTML")
  271. }
  272. // Should at least find the valid ones
  273. foundValidIcon := false
  274. for _, url := range iconURLs {
  275. if url == "https://example.org/valid-before-error.ico" || url == "https://example.org/valid-after-error.ico" {
  276. foundValidIcon = true
  277. break
  278. }
  279. }
  280. if !foundValidIcon {
  281. t.Errorf("Expected to find at least one valid icon URL, got: %v", iconURLs)
  282. }
  283. }
  284. func TestFindIconURLsFromHTMLDocument_EmptyDocument(t *testing.T) {
  285. iconURLs, err := findIconURLsFromHTMLDocument("https://example.org", strings.NewReader(""), "text/html")
  286. if err != nil {
  287. t.Fatal(err)
  288. }
  289. if len(iconURLs) != 0 {
  290. t.Fatalf("Expected 0 icon URLs from empty document, got %d", len(iconURLs))
  291. }
  292. }
  293. func TestResizeIconSmallGif(t *testing.T) {
  294. data, err := base64.StdEncoding.DecodeString("R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==")
  295. if err != nil {
  296. t.Fatal(err)
  297. }
  298. icon := model.Icon{
  299. Content: data,
  300. MimeType: "image/gif",
  301. }
  302. if !bytes.Equal(icon.Content, resizeIcon(&icon).Content) {
  303. t.Fatalf("Converted gif smaller than 16x16")
  304. }
  305. }
  306. func TestResizeIconPng(t *testing.T) {
  307. data, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAACEAAAAhCAYAAABX5MJvAAAALUlEQVR42u3OMQEAAAgDoJnc6BpjDyRgcrcpGwkJCQkJCQkJCQkJCQkJCYmyB7NfUj/Kk4FkAAAAAElFTkSuQmCC")
  308. if err != nil {
  309. t.Fatal(err)
  310. }
  311. icon := model.Icon{
  312. Content: data,
  313. MimeType: "image/png",
  314. }
  315. resizedIcon := resizeIcon(&icon)
  316. if bytes.Equal(data, resizedIcon.Content) {
  317. t.Fatalf("Didn't convert png of 33x33")
  318. }
  319. config, _, err := image.DecodeConfig(bytes.NewReader(resizedIcon.Content))
  320. if err != nil {
  321. t.Fatalf("Couln't decode resulting png: %v", err)
  322. }
  323. if config.Height != 32 || config.Width != 32 {
  324. t.Fatalf("Was expecting an image of 16x16, got %dx%d", config.Width, config.Height)
  325. }
  326. }
  327. func TestResizeIconWebp(t *testing.T) {
  328. data, err := base64.StdEncoding.DecodeString("UklGRkAAAABXRUJQVlA4IDQAAADwAQCdASoBAAEAAQAcJaACdLoB+AAETAAA/vW4f/6aR40jxpHxcP/ugT90CfugT/3NoAAA")
  329. if err != nil {
  330. t.Fatal(err)
  331. }
  332. icon := model.Icon{
  333. Content: data,
  334. MimeType: "image/webp",
  335. }
  336. if !bytes.Equal(icon.Content, resizeIcon(&icon).Content) {
  337. t.Fatalf("Converted webp smaller than 16x16")
  338. }
  339. }
  340. func TestResizeInvalidImage(t *testing.T) {
  341. icon := model.Icon{
  342. Content: []byte("invalid data"),
  343. MimeType: "image/gif",
  344. }
  345. if !bytes.Equal(icon.Content, resizeIcon(&icon).Content) {
  346. t.Fatalf("Tried to convert an invalid image")
  347. }
  348. }
  349. func TestResizeIconTooLargeDimensions(t *testing.T) {
  350. icon := model.Icon{
  351. Content: mustMinimalPNG(t, 4097, 7),
  352. MimeType: "image/png",
  353. }
  354. if resizeIcon(&icon) != nil {
  355. t.Fatalf("Should reject images with too large dimensions")
  356. }
  357. }
  358. func TestResizeIconTooLargePixelCount(t *testing.T) {
  359. icon := model.Icon{
  360. Content: mustMinimalPNG(t, 4096, 4097),
  361. MimeType: "image/png",
  362. }
  363. if resizeIcon(&icon) != nil {
  364. t.Fatalf("Should reject images with too many pixels")
  365. }
  366. }
  367. func TestMinifySvg(t *testing.T) {
  368. data := []byte(`<svg path d=" M1 4h-.001 V1h2v.001 M1 2.6 h1v.001"/></svg>`)
  369. want := []byte(`<svg path="" d="M1 4H.999V1h2v.001M1 2.6h1v.001"/></svg>`)
  370. icon := model.Icon{Content: data, MimeType: "image/svg+xml"}
  371. got := resizeIcon(&icon).Content
  372. if !bytes.Equal(want, got) {
  373. t.Fatalf("Didn't correctly minify the svg: got %s instead of %s", got, want)
  374. }
  375. }
  376. func TestMinifySvgWithError(t *testing.T) {
  377. // Invalid SVG with malformed XML that should cause minification to fail
  378. data := []byte(`<svg><><invalid-tag<>unclosed`)
  379. original := make([]byte, len(data))
  380. copy(original, data)
  381. icon := model.Icon{
  382. Content: data,
  383. MimeType: "image/svg+xml",
  384. }
  385. result := resizeIcon(&icon)
  386. // When minification fails, the original content should be preserved
  387. if !bytes.Equal(original, result.Content) {
  388. t.Fatalf("Expected original content to be preserved on minification error, got %s instead of %s", result.Content, original)
  389. }
  390. // MimeType should remain unchanged
  391. if result.MimeType != "image/svg+xml" {
  392. t.Fatalf("Expected MimeType to remain image/svg+xml, got %s", result.MimeType)
  393. }
  394. }
  395. func mustMinimalPNG(t *testing.T, width, height uint32) []byte {
  396. t.Helper()
  397. var b bytes.Buffer
  398. b.Write([]byte{137, 80, 78, 71, 13, 10, 26, 10})
  399. writePNGChunk(t, &b, "IHDR", func(data []byte) {
  400. binary.BigEndian.PutUint32(data[0:4], width)
  401. binary.BigEndian.PutUint32(data[4:8], height)
  402. data[8] = 8
  403. data[9] = 2
  404. })
  405. writePNGChunk(t, &b, "IEND", nil)
  406. return b.Bytes()
  407. }
  408. func writePNGChunk(t *testing.T, b *bytes.Buffer, chunkType string, fill func([]byte)) {
  409. t.Helper()
  410. dataLen := 0
  411. if chunkType == "IHDR" {
  412. dataLen = 13
  413. }
  414. if err := binary.Write(b, binary.BigEndian, uint32(dataLen)); err != nil {
  415. t.Fatal(err)
  416. }
  417. if _, err := b.WriteString(chunkType); err != nil {
  418. t.Fatal(err)
  419. }
  420. data := make([]byte, dataLen)
  421. if fill != nil {
  422. fill(data)
  423. }
  424. if _, err := b.Write(data); err != nil {
  425. t.Fatal(err)
  426. }
  427. crc := crc32.NewIEEE()
  428. if _, err := crc.Write([]byte(chunkType)); err != nil {
  429. t.Fatal(err)
  430. }
  431. if _, err := crc.Write(data); err != nil {
  432. t.Fatal(err)
  433. }
  434. if err := binary.Write(b, binary.BigEndian, crc.Sum32()); err != nil {
  435. t.Fatal(err)
  436. }
  437. }