Skip to content

Commit b10c057

Browse files
committed
improve nullability annotations
1 parent 34ef278 commit b10c057

File tree

5 files changed

+213
-36
lines changed

5 files changed

+213
-36
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ debug
77
DotSettings.user
88
packages
99
nupkgs
10+
MimeTypeCore/MimeTypeCore.sln.DotSettings.user
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace MimeTypeCore;
2+
3+
#if MODERN
4+
internal static partial class Extensions
5+
{
6+
public static int LastIndexOf(this string source, char value, StringComparison comparisonType)
7+
{
8+
return source.LastIndexOf(value.ToString(), comparisonType);
9+
}
10+
}
11+
#endif
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.IO;
5+
using System.Runtime.CompilerServices;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
11+
namespace MimeTypeCore;
12+
13+
#if !MODERN
14+
internal static partial class Extensions
15+
{
16+
public static int IndexOf(this string source, char value, StringComparison comparisonType)
17+
{
18+
return source.IndexOf(value.ToString(), comparisonType);
19+
}
20+
21+
public static int LastIndexOf(this string source, char value, StringComparison comparisonType)
22+
{
23+
return source.LastIndexOf(value.ToString(), comparisonType);
24+
}
25+
26+
public static bool StartsWith(this string str, char value)
27+
{
28+
return str.Length > 0 && str[0] == value;
29+
}
30+
31+
public static bool StartsWith(this string str, char value, StringComparison comparisonType)
32+
{
33+
if (str.Length == 0)
34+
return false;
35+
36+
return string.Compare(str, 0, value.ToString(), 0, 1, comparisonType) == 0;
37+
}
38+
39+
public static bool TryAdd<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue value)
40+
{
41+
if (dictionary.ContainsKey(key))
42+
return false;
43+
44+
dictionary.Add(key, value);
45+
return true;
46+
}
47+
48+
public static bool TryAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue value)
49+
{
50+
if (dictionary.ContainsKey(key))
51+
return false;
52+
53+
dictionary.Add(key, value);
54+
return true;
55+
}
56+
57+
public static TValue? GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key)
58+
{
59+
return dictionary.TryGetValue(key, out TValue value) ? value : default;
60+
}
61+
62+
public static TValue GetValueOrDefault<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue)
63+
{
64+
return dictionary.TryGetValue(key, out TValue value) ? value : defaultValue;
65+
}
66+
67+
public static TValue? GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
68+
{
69+
return dictionary.TryGetValue(key, out TValue value) ? value : default;
70+
}
71+
72+
public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue)
73+
{
74+
return dictionary.TryGetValue(key, out TValue value) ? value : defaultValue;
75+
}
76+
77+
public static double Clamp(this double value, double min, double max)
78+
{
79+
if (value < min)
80+
return min;
81+
return value > max ? max : value;
82+
}
83+
84+
public static float Clamp(this float value, float min, float max)
85+
{
86+
if (value < min)
87+
return min;
88+
return value > max ? max : value;
89+
}
90+
91+
public static int Clamp(this int value, int min, int max)
92+
{
93+
if (value < min)
94+
return min;
95+
return value > max ? max : value;
96+
}
97+
98+
public static long Clamp(this long value, long min, long max)
99+
{
100+
if (value < min)
101+
return min;
102+
return value > max ? max : value;
103+
}
104+
}
105+
#endif

MimeTypeCore/MimeTypeCore/MimeTypeCore.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,4 @@
3030
<_Parameter1>MimeTypeCore.Inserter</_Parameter1>
3131
</AssemblyAttribute>
3232
</ItemGroup>
33-
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
34-
<PackageReference Include="Microsoft.Bcl.Async" Version="1.0.168" />
35-
</ItemGroup>
3633
</Project>

MimeTypeCore/MimeTypeCore/MimeTypeMap.cs

Lines changed: 96 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Collections.Frozen;
44
#endif
55
using System.Collections.Generic;
6+
using System.Diagnostics.CodeAnalysis;
67
using System.IO;
78
using System.Linq;
89

@@ -13,24 +14,69 @@ namespace MimeTypeCore;
1314
/// </summary>
1415
public 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

Comments
 (0)