11#nullable enable
22
3+ using System . Buffers . Binary ;
34using System . Text ;
45using AngleSharp . Dom ;
56using Bit . Icons . Extensions ;
@@ -13,25 +14,28 @@ public class IconLink
1314 private static readonly HashSet < string > _blocklistedRels = new ( StringComparer . InvariantCultureIgnoreCase ) { "preload" , "image_src" , "preconnect" , "canonical" , "alternate" , "stylesheet" } ;
1415 private static readonly HashSet < string > _iconExtensions = new ( StringComparer . InvariantCultureIgnoreCase ) { ".ico" , ".png" , ".jpg" , ".jpeg" } ;
1516 private const string _pngMediaType = "image/png" ;
16- private static readonly byte [ ] _pngHeader = new byte [ ] { 137 , 80 , 78 , 71 } ;
17+ private static readonly byte [ ] _pngHeader = [ 137 , 80 , 78 , 71 ] ;
18+ private static readonly byte [ ] _pngSignature = [ 137 , 80 , 78 , 71 , 13 , 10 , 26 , 10 ] ;
1719 private static readonly byte [ ] _webpHeader = Encoding . UTF8 . GetBytes ( "RIFF" ) ;
1820
21+ private static readonly HashSet < string > _allowedPngChunkTypes = new ( StringComparer . Ordinal )
22+ {
23+ "IHDR" , "PLTE" , "IDAT" , "IEND" , "tRNS" , "sRGB" , "gAMA" , "cHRM" ,
24+ } ;
25+
1926 private const string _icoMediaType = "image/x-icon" ;
2027 private const string _icoAltMediaType = "image/vnd.microsoft.icon" ;
2128 private static readonly byte [ ] _icoHeader = new byte [ ] { 00 , 00 , 01 , 00 } ;
2229
2330 private const string _jpegMediaType = "image/jpeg" ;
2431 private static readonly byte [ ] _jpegHeader = new byte [ ] { 255 , 216 , 255 } ;
2532
26- private const string _svgXmlMediaType = "image/svg+xml" ;
27-
2833 private static readonly HashSet < string > _allowedMediaTypes = new ( StringComparer . InvariantCultureIgnoreCase )
2934 {
3035 _pngMediaType ,
3136 _icoMediaType ,
3237 _icoAltMediaType ,
3338 _jpegMediaType ,
34- _svgXmlMediaType ,
3539 } ;
3640
3741 private bool _useUriDirectly = false ;
@@ -156,6 +160,20 @@ public bool IsUsable()
156160 return null ;
157161 }
158162
163+ if ( HeaderMatch ( bytes , _pngHeader ) )
164+ {
165+ bytes = StripPngMetadata ( bytes ) ;
166+ }
167+ else if ( HeaderMatch ( bytes , _icoHeader ) )
168+ {
169+ bytes = StripIcoEmbeddedPngMetadata ( bytes ) ;
170+ }
171+
172+ if ( bytes == null )
173+ {
174+ return null ;
175+ }
176+
159177 return new Icon { Image = bytes , Format = format } ;
160178 }
161179
@@ -217,4 +235,126 @@ private static string DetermineImageFormatFromFile(byte[] imageBytes)
217235 return string . Empty ;
218236 }
219237 }
238+
239+ internal static byte [ ] ? StripPngMetadata ( byte [ ] bytes )
240+ {
241+ if ( bytes . Length < _pngSignature . Length || ! HeaderMatch ( bytes , _pngSignature ) )
242+ {
243+ return null ;
244+ }
245+
246+ using var output = new MemoryStream ( bytes . Length ) ;
247+ output . Write ( bytes , 0 , _pngSignature . Length ) ;
248+
249+ var offset = _pngSignature . Length ;
250+ var seenIend = false ;
251+ while ( offset + 12 <= bytes . Length )
252+ {
253+ var length = BinaryPrimitives . ReadInt32BigEndian ( bytes . AsSpan ( offset , 4 ) ) ;
254+ if ( length < 0 || ( long ) offset + 12 + length > bytes . Length )
255+ {
256+ return null ;
257+ }
258+
259+ var type = Encoding . ASCII . GetString ( bytes , offset + 4 , 4 ) ;
260+ var chunkSize = 12 + length ;
261+
262+ if ( _allowedPngChunkTypes . Contains ( type ) )
263+ {
264+ output . Write ( bytes , offset , chunkSize ) ;
265+ }
266+
267+ offset += chunkSize ;
268+
269+ if ( type == "IEND" )
270+ {
271+ seenIend = true ;
272+ break ;
273+ }
274+ }
275+
276+ if ( ! seenIend )
277+ {
278+ return null ;
279+ }
280+
281+ return output . ToArray ( ) ;
282+ }
283+
284+ internal static byte [ ] ? StripIcoEmbeddedPngMetadata ( byte [ ] bytes )
285+ {
286+ const int dirHeaderSize = 6 ;
287+ const int entrySize = 16 ;
288+
289+ if ( bytes . Length < dirHeaderSize || ! HeaderMatch ( bytes , _icoHeader ) )
290+ {
291+ return null ;
292+ }
293+
294+ const int maxEntries = 64 ;
295+ var count = BinaryPrimitives . ReadUInt16LittleEndian ( bytes . AsSpan ( 4 , 2 ) ) ;
296+ if ( count == 0 || count > maxEntries )
297+ {
298+ return null ;
299+ }
300+
301+ var directorySize = dirHeaderSize + count * entrySize ;
302+ if ( bytes . Length < directorySize )
303+ {
304+ return null ;
305+ }
306+
307+ var entries = new byte [ count ] [ ] ;
308+ var images = new byte [ count ] [ ] ;
309+
310+ for ( var i = 0 ; i < count ; i ++ )
311+ {
312+ var entryOffset = dirHeaderSize + i * entrySize ;
313+ var size = BinaryPrimitives . ReadInt32LittleEndian ( bytes . AsSpan ( entryOffset + 8 , 4 ) ) ;
314+ var dataOffset = BinaryPrimitives . ReadInt32LittleEndian ( bytes . AsSpan ( entryOffset + 12 , 4 ) ) ;
315+
316+ if ( size < 0 || dataOffset < directorySize || ( long ) dataOffset + size > bytes . Length )
317+ {
318+ return null ;
319+ }
320+
321+ var image = new byte [ size ] ;
322+ Buffer . BlockCopy ( bytes , dataOffset , image , 0 , size ) ;
323+
324+ if ( HeaderMatch ( image , _pngHeader ) )
325+ {
326+ var stripped = StripPngMetadata ( image ) ;
327+ if ( stripped == null )
328+ {
329+ return null ;
330+ }
331+ image = stripped ;
332+ }
333+
334+ var entry = new byte [ entrySize ] ;
335+ Buffer . BlockCopy ( bytes , entryOffset , entry , 0 , entrySize ) ;
336+
337+ entries [ i ] = entry ;
338+ images [ i ] = image ;
339+ }
340+
341+ using var output = new MemoryStream ( bytes . Length ) ;
342+ output . Write ( bytes , 0 , dirHeaderSize ) ;
343+
344+ var imageOffset = directorySize ;
345+ for ( var i = 0 ; i < count ; i ++ )
346+ {
347+ BinaryPrimitives . WriteInt32LittleEndian ( entries [ i ] . AsSpan ( 8 , 4 ) , images [ i ] . Length ) ;
348+ BinaryPrimitives . WriteInt32LittleEndian ( entries [ i ] . AsSpan ( 12 , 4 ) , imageOffset ) ;
349+ output . Write ( entries [ i ] , 0 , entrySize ) ;
350+ imageOffset += images [ i ] . Length ;
351+ }
352+
353+ for ( var i = 0 ; i < count ; i ++ )
354+ {
355+ output . Write ( images [ i ] , 0 , images [ i ] . Length ) ;
356+ }
357+
358+ return output . ToArray ( ) ;
359+ }
220360}
0 commit comments