Skip to content

Commit b411a6d

Browse files
committed
feat: sync MIME metadata from IANA
1 parent 5eaed5b commit b411a6d

17 files changed

Lines changed: 51170 additions & 140 deletions

.github/workflows/mime-sync.yml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,50 @@ jobs:
2626
run: dotnet restore
2727

2828
- name: Run MIME sync tool
29-
run: dotnet run --project ManagedCode.MimeTypes.Sync --configuration Release -- --prefer-remote
29+
run: >
30+
dotnet run --project ManagedCode.MimeTypes.Sync --configuration Release --
31+
--prefer-remote
32+
--template-concurrency 8
33+
34+
- name: Detect MIME database changes
35+
id: mime_changes
36+
shell: bash
37+
run: |
38+
if git diff --quiet -- ManagedCode.MimeTypes/mimeTypes.json ManagedCode.MimeTypes/mimeTypes.metadata.json; then
39+
echo "changed=false" >> "$GITHUB_OUTPUT"
40+
echo "No MIME database changes detected." >> "$GITHUB_STEP_SUMMARY"
41+
else
42+
echo "changed=true" >> "$GITHUB_OUTPUT"
43+
echo "MIME database changes detected." >> "$GITHUB_STEP_SUMMARY"
44+
fi
45+
46+
- name: Bump patch package version
47+
if: steps.mime_changes.outputs.changed == 'true'
48+
shell: bash
49+
run: |
50+
set -euo pipefail
51+
52+
current_version="$(sed -nE 's#.*<Version>([^<]+)</Version>.*#\1#p' Directory.Build.props | head -n 1)"
53+
package_version="$(sed -nE 's#.*<PackageVersion>([^<]+)</PackageVersion>.*#\1#p' Directory.Build.props | head -n 1)"
54+
55+
if [[ "$current_version" != "$package_version" ]]; then
56+
echo "Version and PackageVersion differ: $current_version vs $package_version" >&2
57+
exit 1
58+
fi
59+
60+
if [[ ! "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
61+
echo "Unsupported version format: $current_version" >&2
62+
exit 1
63+
fi
64+
65+
IFS='.' read -r major minor patch <<< "$current_version"
66+
next_version="$major.$minor.$((patch + 1))"
67+
perl -0pi -e "s#<Version>\\Q$current_version\\E</Version>#<Version>$next_version</Version>#; s#<PackageVersion>\\Q$current_version\\E</PackageVersion>#<PackageVersion>$next_version</PackageVersion>#;" Directory.Build.props
68+
69+
echo "Bumped package version: $current_version -> $next_version" | tee -a "$GITHUB_STEP_SUMMARY"
70+
71+
- name: Test synced database
72+
run: dotnet test --configuration Release --no-restore --verbosity normal
3073

3174
- name: Create Pull Request
3275
uses: peter-evans/create-pull-request@v6
@@ -35,7 +78,11 @@ jobs:
3578
commit-message: "chore: sync MIME database"
3679
title: "chore: sync MIME database"
3780
body: |
38-
Automated update of MIME database from configured sources.
81+
Automated update of the MIME database from the IANA media types registry,
82+
mime-db, Apache mime.types, and curated ManagedCode overrides.
83+
84+
When MIME data changes, this PR also bumps the package patch version.
85+
See the workflow summary for the IANA registry date, source counts, and version bump.
3986
branch: chore/sync-mime-database
4087
delete-branch: true
4188
labels: |

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
<RepositoryUrl>https://github.com/managedcode/MimeTypes</RepositoryUrl>
1818
<PackageProjectUrl>https://github.com/managedcode/MimeTypes</PackageProjectUrl>
1919
<Product>Managed Code - MimeTypes</Product>
20-
<Version>10.0.1</Version>
21-
<PackageVersion>10.0.1</PackageVersion>
20+
<Version>10.0.2</Version>
21+
<PackageVersion>10.0.2</PackageVersion>
2222

2323
</PropertyGroup>
2424
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">

ManagedCode.MimeTypes.Generator/ManagedCode.MimeTypes.Generator.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<PropertyGroup>
44
<LangVersion>14</LangVersion>
55
<IsPackable>false</IsPackable>
6+
<Nullable>enable</Nullable>
67
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
78
</PropertyGroup>
89

ManagedCode.MimeTypes.Generator/MimeTypeSourceGenerator.cs

Lines changed: 228 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ public void Execute(GeneratorExecutionContext context)
6262
using var document = JsonDocument.Parse(json);
6363

6464
StringBuilder defineDictionaryBuilder = new();
65+
StringBuilder metadataBuilder = new();
6566
StringBuilder propertyBuilder = new();
6667
Dictionary<string, string> types = new(StringComparer.OrdinalIgnoreCase);
6768

@@ -79,6 +80,20 @@ public void Execute(GeneratorExecutionContext context)
7980
types[ParseKey(extension)] = mimeValue;
8081
}
8182

83+
var metadataPath = GetMetadataPath(mimeTypesPath, context);
84+
if (File.Exists(metadataPath))
85+
{
86+
using var metadataDocument = JsonDocument.Parse(File.ReadAllBytes(metadataPath));
87+
foreach (var item in metadataDocument.RootElement.EnumerateObject())
88+
{
89+
var initializer = BuildMimeTypeInfoInitializer(item.Name, item.Value);
90+
if (initializer.Length > 0)
91+
{
92+
metadataBuilder.AppendLine($"RegisterMimeTypeInfoInternal({initializer});");
93+
}
94+
}
95+
}
96+
8297
foreach (var item in types)
8398
{
8499
propertyBuilder.AppendLine($"public static string {item.Key} => \"{Escape(item.Value)}\";");
@@ -87,13 +102,17 @@ public void Execute(GeneratorExecutionContext context)
87102
context.ReportDiagnostic(Diagnostic.Create(MimeTypesLoadedDiagnostic, Location.None, types.Count));
88103

89104
context.AddSource("MimeHelper.Properties.cs", SourceText.From(@$"
105+
using System;
106+
using System.Collections.Immutable;
107+
90108
namespace ManagedCode.MimeTypes
91109
{{
92110
public static partial class MimeHelper
93111
{{
94112
static partial void Init()
95113
{{
96114
{defineDictionaryBuilder}
115+
{metadataBuilder}
97116
}}
98117
{propertyBuilder}
99118
}}
@@ -117,6 +136,13 @@ static partial void Init()
117136

118137
private string GetMimeTypesPath(GeneratorExecutionContext context)
119138
{
139+
var additionalFile = context.AdditionalFiles.FirstOrDefault(static file =>
140+
string.Equals(Path.GetFileName(file.Path), "mimeTypes.json", StringComparison.OrdinalIgnoreCase));
141+
if (additionalFile != null)
142+
{
143+
return additionalFile.Path;
144+
}
145+
120146
// Try to find mimeTypes.json in the project directory
121147
var compilation = context.Compilation;
122148
var projectDir = Path.GetDirectoryName(compilation.SyntaxTrees.First().FilePath);
@@ -136,6 +162,200 @@ private string GetMimeTypesPath(GeneratorExecutionContext context)
136162
return possiblePaths.FirstOrDefault(File.Exists) ?? possiblePaths[0];
137163
}
138164

165+
private string GetMetadataPath(string mimeTypesPath, GeneratorExecutionContext context)
166+
{
167+
var additionalFile = context.AdditionalFiles.FirstOrDefault(static file =>
168+
string.Equals(Path.GetFileName(file.Path), "mimeTypes.metadata.json", StringComparison.OrdinalIgnoreCase));
169+
if (additionalFile != null)
170+
{
171+
return additionalFile.Path;
172+
}
173+
174+
var mimeTypesDirectory = Path.GetDirectoryName(mimeTypesPath);
175+
var compilation = context.Compilation;
176+
var projectDir = Path.GetDirectoryName(compilation.SyntaxTrees.First().FilePath);
177+
178+
var possiblePaths = new[]
179+
{
180+
Path.Combine(mimeTypesDirectory ?? "", "mimeTypes.metadata.json"),
181+
Path.Combine(Directory.GetCurrentDirectory(), "mimeTypes.metadata.json"),
182+
Path.Combine(projectDir ?? "", "mimeTypes.metadata.json"),
183+
Path.Combine(Directory.GetParent(projectDir ?? "")?.FullName ?? "", "mimeTypes.metadata.json"),
184+
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mimeTypes.metadata.json")
185+
};
186+
187+
return possiblePaths.FirstOrDefault(File.Exists) ?? possiblePaths[0];
188+
}
189+
190+
private static string BuildMimeTypeInfoInitializer(string fallbackMime, JsonElement element)
191+
{
192+
if (element.ValueKind != JsonValueKind.Object)
193+
{
194+
return string.Empty;
195+
}
196+
197+
var mime = GetString(element, "mime") ?? fallbackMime;
198+
if (string.IsNullOrWhiteSpace(mime))
199+
{
200+
return string.Empty;
201+
}
202+
203+
var builder = new StringBuilder();
204+
builder.AppendLine("new MimeTypeInfo");
205+
builder.AppendLine("{");
206+
builder.AppendLine($"Mime = {Literal(mime)},");
207+
builder.AppendLine($"Extensions = {StringArray(GetStringArray(element, "extensions"))},");
208+
builder.AppendLine($"IsIanaRegistered = {BoolLiteral(GetBool(element, "isIanaRegistered"))},");
209+
builder.AppendLine($"IsObsolete = {BoolLiteral(GetBool(element, "isObsolete"))},");
210+
AppendNullableString(builder, "PreferredMime", GetString(element, "preferredMime"));
211+
AppendNullableString(builder, "Template", GetString(element, "template"));
212+
AppendNullableString(builder, "TemplateUrl", GetString(element, "templateUrl"));
213+
AppendNullableString(builder, "Source", GetString(element, "source"));
214+
AppendNullableString(builder, "Registered", GetString(element, "registered"));
215+
AppendNullableString(builder, "Updated", GetString(element, "updated"));
216+
AppendNullableString(builder, "IntendedUsage", GetString(element, "intendedUsage"));
217+
AppendNullableString(builder, "EncodingConsiderations", GetString(element, "encodingConsiderations"));
218+
AppendNullableString(builder, "PublishedSpecification", GetString(element, "publishedSpecification"));
219+
AppendNullableString(builder, "Applications", GetString(element, "applications"));
220+
builder.AppendLine($"DeprecatedAliases = {StringArray(GetStringArray(element, "deprecatedAliases"))},");
221+
builder.AppendLine($"References = {StringArray(GetStringArray(element, "references"))},");
222+
builder.AppendLine($"MagicSignatures = {MagicSignatureArray(element)},");
223+
builder.Append('}');
224+
return builder.ToString();
225+
}
226+
227+
private static void AppendNullableString(StringBuilder builder, string propertyName, string? value)
228+
{
229+
if (value == null)
230+
{
231+
return;
232+
}
233+
234+
builder.AppendLine($"{propertyName} = {Literal(value)},");
235+
}
236+
237+
private static string MagicSignatureArray(JsonElement element)
238+
{
239+
if (!element.TryGetProperty("magicSignatures", out var signatures) || signatures.ValueKind != JsonValueKind.Array)
240+
{
241+
return "Array.Empty<MimeMagicSignature>()";
242+
}
243+
244+
var items = new List<string>();
245+
foreach (var signature in signatures.EnumerateArray())
246+
{
247+
if (signature.ValueKind != JsonValueKind.Object)
248+
{
249+
continue;
250+
}
251+
252+
var raw = GetString(signature, "raw");
253+
if (string.IsNullOrWhiteSpace(raw))
254+
{
255+
continue;
256+
}
257+
258+
var bytes = GetByteArray(signature, "bytes");
259+
var hex = GetString(signature, "hex");
260+
var offset = GetInt(signature, "offset");
261+
items.Add($@"new MimeMagicSignature
262+
{{
263+
Raw = {Literal(raw)},
264+
Bytes = {ByteArray(bytes)},
265+
Hex = {Literal(hex)},
266+
Offset = {offset.ToString(System.Globalization.CultureInfo.InvariantCulture)}
267+
}}");
268+
}
269+
270+
return items.Count == 0
271+
? "Array.Empty<MimeMagicSignature>()"
272+
: "new[]\n{\n" + string.Join(",\n", items) + "\n}";
273+
}
274+
275+
private static string StringArray(IReadOnlyList<string> values)
276+
{
277+
return values.Count == 0
278+
? "Array.Empty<string>()"
279+
: "new[] { " + string.Join(", ", values.Select(Literal)) + " }";
280+
}
281+
282+
private static string ByteArray(IReadOnlyList<byte> values)
283+
{
284+
return values.Count == 0
285+
? "ImmutableArray<byte>.Empty"
286+
: "ImmutableArray.Create<byte>(" + string.Join(", ", values.Select(static value => $"0x{value:X2}")) + ")";
287+
}
288+
289+
private static IReadOnlyList<string> GetStringArray(JsonElement element, string propertyName)
290+
{
291+
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
292+
{
293+
return Array.Empty<string>();
294+
}
295+
296+
var values = new List<string>();
297+
foreach (var item in property.EnumerateArray())
298+
{
299+
if (item.ValueKind == JsonValueKind.String)
300+
{
301+
var value = item.GetString();
302+
if (!string.IsNullOrWhiteSpace(value))
303+
{
304+
values.Add(value);
305+
}
306+
}
307+
}
308+
309+
return values;
310+
}
311+
312+
private static IReadOnlyList<byte> GetByteArray(JsonElement element, string propertyName)
313+
{
314+
if (!element.TryGetProperty(propertyName, out var property) || property.ValueKind != JsonValueKind.Array)
315+
{
316+
return Array.Empty<byte>();
317+
}
318+
319+
var values = new List<byte>();
320+
foreach (var item in property.EnumerateArray())
321+
{
322+
if (item.ValueKind == JsonValueKind.Number && item.TryGetByte(out var value))
323+
{
324+
values.Add(value);
325+
}
326+
}
327+
328+
return values;
329+
}
330+
331+
private static string? GetString(JsonElement element, string propertyName)
332+
{
333+
return element.TryGetProperty(propertyName, out var property) && property.ValueKind == JsonValueKind.String
334+
? property.GetString()
335+
: null;
336+
}
337+
338+
private static bool GetBool(JsonElement element, string propertyName)
339+
{
340+
return element.TryGetProperty(propertyName, out var property) &&
341+
property.ValueKind is JsonValueKind.True or JsonValueKind.False &&
342+
property.GetBoolean();
343+
}
344+
345+
private static int GetInt(JsonElement element, string propertyName)
346+
{
347+
return element.TryGetProperty(propertyName, out var property) &&
348+
property.ValueKind == JsonValueKind.Number &&
349+
property.TryGetInt32(out var value)
350+
? value
351+
: 0;
352+
}
353+
354+
private static string BoolLiteral(bool value)
355+
{
356+
return value ? "true" : "false";
357+
}
358+
139359
private static string ParseKey(string key)
140360
{
141361
if (char.IsDigit(key[0]))
@@ -148,10 +368,17 @@ private static string ParseKey(string key)
148368
return key.ToUpperInvariant();
149369
}
150370

371+
private static string Literal(string? value)
372+
{
373+
return value == null ? "null" : $"\"{Escape(value)}\"";
374+
}
375+
151376
private static string Escape(string value)
152377
{
153378
return value
154379
.Replace("\\", "\\\\")
155-
.Replace("\"", "\\\"");
380+
.Replace("\"", "\\\"")
381+
.Replace("\r", "\\r")
382+
.Replace("\n", "\\n");
156383
}
157384
}

ManagedCode.MimeTypes.Sync/ManagedCode.MimeTypes.Sync.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,4 @@
77
<Nullable>enable</Nullable>
88
<ImplicitUsings>enable</ImplicitUsings>
99
</PropertyGroup>
10-
<ItemGroup>
11-
<PackageReference Include="System.Text.Json" Version="8.0.5" />
12-
</ItemGroup>
1310
</Project>

0 commit comments

Comments
 (0)