33using System . Collections . Frozen ;
44#endif
55using System . Collections . Generic ;
6+ using System . Diagnostics . CodeAnalysis ;
67using System . IO ;
78using System . Linq ;
89
@@ -13,24 +14,69 @@ namespace MimeTypeCore;
1314/// </summary>
1415public static class MimeTypeMap
1516{
16- private const string Dot = "." ;
17- private const string QuestionMark = "?" ;
18- private const string DefaultMimeType = "application/octet-stream" ;
17+ private const char Dot = '.' ;
18+ private const char QuestionMark = '?' ;
1919#if MODERN
20- private static readonly FrozenDictionary < string , string > mappings = MimeTypeMapMapping . Mappings ;
20+ private static FrozenDictionary < string , string > mappings = MimeTypeMapMapping . Mappings ;
2121#else
2222 private static readonly Dictionary < string , string > mappings = MimeTypeMapMapping . Mappings ;
2323#endif
2424
25+ /// <summary>
26+ /// Adds custom extension-MIME pairs to the underlying dictionary.<br/>
27+ /// Note: On .NET 8+ the backing collection is a FrozenDictionary, calling this reconstructs the entire collection.
28+ /// </summary>
29+ /// <param name="pairs">For example: [{".png", "image/png"}].</param>
30+ public static void AddMimeTypes ( IEnumerable < KeyValuePair < string , string > > pairs )
31+ {
32+ #if MODERN
33+ Dictionary < string , string > newMappings = new Dictionary < string , string > ( MimeTypeMapMapping . Mappings , StringComparer . OrdinalIgnoreCase ) ;
34+ #endif
35+
36+ foreach ( KeyValuePair < string , string > pair in pairs )
37+ {
38+ string key = pair . Key ;
39+ string val = pair . Value ;
40+
41+ if ( ! key . StartsWith ( '.' ) )
42+ {
43+ key = $ ".{ key . Trim ( ) } ";
44+ }
45+
46+ key = key . ToLowerInvariant ( ) . Trim ( ) ;
47+ val = val . ToLowerInvariant ( ) . Trim ( ) ;
48+
49+ #if ! MODERN
50+ MimeTypeMapMapping . Mappings [ key ] = val ;
51+ #else
52+ newMappings [ key ] = val ;
53+ #endif
54+ }
55+
56+ #if MODERN
57+ mappings = newMappings . ToFrozenDictionary ( ) ;
58+ #endif
59+ }
60+
61+ #if MODERN
2562 /// <summary>
2663 /// Tries to get the type of the MIME from the provided string (filename or extension).
2764 /// This method relies solely on the file extension and does not read file content.
2865 /// </summary>
2966 /// <param name="str">The filename or extension (e.g., "document.pdf" or "pdf").</param>
3067 /// <param name="mimeType">The variable to store the MIME type.</param>
3168 /// <returns>True if a MIME type was found for the extension, false otherwise.</returns>
32- /// <exception cref="ArgumentNullException" />
33- public static bool TryGetMimeType ( string str , out string mimeType )
69+ public static bool TryGetMimeType ( string str , [ NotNullWhen ( true ) ] out string ? mimeType )
70+ #else
71+ /// <summary>
72+ /// Tries to get the type of the MIME from the provided string (filename or extension).
73+ /// This method relies solely on the file extension and does not read file content.
74+ /// </summary>
75+ /// <param name="str">The filename or extension (e.g., "document.pdf" or "pdf").</param>
76+ /// <param name="mimeType">The variable to store the MIME type.</param>
77+ /// <returns>True if a MIME type was found for the extension, false otherwise.</returns>
78+ public static bool TryGetMimeType ( string str , out string ? mimeType )
79+ #endif
3480 {
3581 int indexQuestionMark = str . IndexOf ( QuestionMark , StringComparison . Ordinal ) ;
3682
@@ -47,11 +93,7 @@ public static bool TryGetMimeType(string str, out string mimeType)
4793
4894 if ( index != - 1 && str . Length > index + 1 )
4995 {
50- #if NET8_0_OR_GREATER
51- extension = string . Concat ( Dot , str . AsSpan ( index + 1 ) ) ;
52- #else
5396 extension = $ "{ Dot } { str . Substring ( index + 1 ) } ". ToLowerInvariant ( ) ;
54- #endif
5597 }
5698 else
5799 {
@@ -61,12 +103,7 @@ public static bool TryGetMimeType(string str, out string mimeType)
61103 else
62104 {
63105 int lastDotIndex = str . LastIndexOf ( '.' ) ;
64-
65- #if NET8_0_OR_GREATER
66- extension = string . Concat ( Dot , str . AsSpan ( lastDotIndex + 1 ) ) ;
67- #else
68106 extension = $ "{ Dot } { str . Substring ( lastDotIndex + 1 ) } ". ToLowerInvariant ( ) ;
69- #endif
70107 }
71108
72109 return mappings . TryGetValue ( extension , out mimeType ) ;
@@ -78,12 +115,23 @@ public static bool TryGetMimeType(string str, out string mimeType)
78115 /// </summary>
79116 /// <param name="str">The filename or extension.</param>
80117 /// <returns>The MIME type or "application/octet-stream" if not found.</returns>
81- /// <exception cref="ArgumentNullException" />
82- public static string GetMimeType ( string str )
118+ public static string ? GetMimeType ( string str )
83119 {
84- return TryGetMimeType ( str , out string result ) ? result : DefaultMimeType ;
120+ TryGetMimeType ( str , out string ? result ) ;
121+ return result ;
85122 }
86123
124+ #if MODERN
125+ /// <summary>
126+ /// Tries to get the MIME type from a file stream, using magic bytes for more accurate detection and collision resolution.
127+ /// If magic bytes don't provide a definitive answer, it falls back to extension-based lookup.
128+ /// </summary>
129+ /// <param name="filename">Filename hint (e.g., "document.ts") to help resolve collisions, especially for ZIP-based formats or text files.</param>
130+ /// <param name="fileStream">The file stream. It will be read from its current position and then reset. The stream must support synchronous reading.</param>
131+ /// <param name="mimeType">The detected MIME type.</param>
132+ /// <returns>True if a MIME type was successfully determined, false otherwise.</returns>
133+ public static bool TryGetMimeType ( string filename , Stream fileStream , [ NotNullWhen ( true ) ] out string ? mimeType )
134+ #else
87135 /// <summary>
88136 /// Tries to get the MIME type from a file stream, using magic bytes for more accurate detection and collision resolution.
89137 /// If magic bytes don't provide a definitive answer, it falls back to extension-based lookup.
@@ -92,10 +140,9 @@ public static string GetMimeType(string str)
92140 /// <param name="fileStream">The file stream. It will be read from its current position and then reset. The stream must support synchronous reading.</param>
93141 /// <param name="mimeType">The detected MIME type.</param>
94142 /// <returns>True if a MIME type was successfully determined, false otherwise.</returns>
95- public static bool TryGetMimeType ( string filename , Stream fileStream , out string mimeType )
143+ public static bool TryGetMimeType ( string filename , Stream fileStream , out string ? mimeType )
144+ #endif
96145 {
97- mimeType = DefaultMimeType ;
98-
99146 if ( ! fileStream . CanRead )
100147 {
101148 return TryGetMimeType ( filename , out mimeType ) ;
@@ -150,7 +197,7 @@ public static bool TryGetMimeType(string filename, Stream fileStream, out string
150197 /// <returns>MIME type if detected, otherwise null.</returns>
151198 public static async Task < string ? > TryGetMimeTypeAsync ( string filename , Stream fileStream , CancellationToken token = default )
152199 {
153- string mimeType ;
200+ string ? mimeType ;
154201
155202 if ( ! fileStream . CanRead )
156203 {
@@ -213,10 +260,8 @@ public static bool TryGetMimeType(string filename, Stream fileStream, out string
213260 }
214261 }
215262
216- private static bool ProcessMagicBytes ( byte [ ] headerBytes , string filename , out string mimeType )
263+ private static bool ProcessMagicBytes ( byte [ ] headerBytes , string filename , out string ? mimeType )
217264 {
218- mimeType = DefaultMimeType ;
219-
220265 List < Info > magicByteMatches = MagicByteDetector . Detect ( headerBytes ) ;
221266
222267 if ( magicByteMatches . Count == 0 )
@@ -239,7 +284,9 @@ private static bool ProcessMagicBytes(byte[] headerBytes, string filename, out s
239284 private static string ? GetFileExtension ( string filename )
240285 {
241286 if ( string . IsNullOrEmpty ( filename ) )
287+ {
242288 return null ;
289+ }
243290
244291 int lastDotIndex = filename . LastIndexOf ( '.' ) ;
245292
@@ -250,7 +297,6 @@ private static bool ProcessMagicBytes(byte[] headerBytes, string filename, out s
250297
251298 return null ;
252299 }
253-
254300
255301 private static Info ? SelectBestMatch ( List < Info > magicByteMatches , string ? preferredExtension )
256302 {
@@ -271,8 +317,7 @@ private static bool ProcessMagicBytes(byte[] headerBytes, string filename, out s
271317 if ( magicByteMatches . Count > 1 )
272318 {
273319 // PNG vs APNG
274- if ( magicByteMatches . Any ( m => m . TypeName == "png" ) &&
275- magicByteMatches . Any ( m => m . TypeName == "apng" ) )
320+ if ( magicByteMatches . Any ( m => m . TypeName == "png" ) && magicByteMatches . Any ( m => m . TypeName == "apng" ) )
276321 {
277322 return magicByteMatches . FirstOrDefault ( m =>
278323 m . TypeName == "apng" && preferredExtension == "apng" ) ??
@@ -295,13 +340,23 @@ private static bool ProcessMagicBytes(byte[] headerBytes, string filename, out s
295340 return magicByteMatches . FirstOrDefault ( ) ;
296341 }
297342
343+ #if MODERN
344+ /// <summary>
345+ /// Gets the MIME type from a file stream, using magic bytes for more accurate detection and collision resolution.
346+ /// </summary>
347+ /// <param name="fileStream">The file stream. It will be read from its current position and then reset.</param>
348+ /// <param name="mimeType">The detected MIME type.</param>
349+ /// <returns>True if a MIME type was successfully determined, false otherwise.</returns>
350+ public static bool TryGetMimeType ( Stream fileStream , [ NotNullWhen ( true ) ] out string ? mimeType )
351+ #else
298352 /// <summary>
299353 /// Gets the MIME type from a file stream, using magic bytes for more accurate detection and collision resolution.
300354 /// </summary>
301355 /// <param name="fileStream">The file stream. It will be read from its current position and then reset.</param>
302356 /// <param name="mimeType">The detected MIME type.</param>
303357 /// <returns>True if a MIME type was successfully determined, false otherwise.</returns>
304- public static bool TryGetMimeType ( Stream fileStream , out string mimeType )
358+ public static bool TryGetMimeType ( Stream fileStream , out string ? mimeType )
359+ #endif
305360 {
306361 string fileName = string . Empty ;
307362
@@ -314,21 +369,29 @@ public static bool TryGetMimeType(Stream fileStream, out string mimeType)
314369
315370 return TryGetMimeType ( fileName , fileStream , out mimeType ) ;
316371 }
317-
372+
373+ #if MODERN
374+ /// <summary>
375+ /// Gets the extension from the provided MIME type.
376+ /// </summary>
377+ /// <param name="mimeType">Type of the MIME.</param>
378+ /// <param name="extension">Extension of the file.</param>
379+ /// <returns>The extension.</returns>
380+ public static bool TryGetExtension ( string mimeType , [ NotNullWhen ( true ) ] out string ? extension )
381+ #else
318382 /// <summary>
319383 /// Gets the extension from the provided MIME type.
320384 /// </summary>
321385 /// <param name="mimeType">Type of the MIME.</param>
322386 /// <param name="extension">Extension of the file.</param>
323387 /// <returns>The extension.</returns>
324- /// <exception cref="ArgumentNullException" />
325- /// <exception cref="ArgumentException" />
326388 public static bool TryGetExtension ( string mimeType , out string ? extension )
389+ #endif
327390 {
328391#if ! MODERN
329392 mimeType = mimeType . ToLowerInvariant ( ) ;
330393#endif
331394
332- return mimeType . StartsWith ( Dot ) ? throw new ArgumentException ( "Requested mime type is not valid: " + mimeType ) : mappings . TryGetValue ( mimeType , out extension ) ;
395+ return mappings . TryGetValue ( mimeType , out extension ) ;
333396 }
334397}
0 commit comments