Skip to content

Commit dbc0a56

Browse files
authored
Merge pull request #343 from drewnoakes/apple-run-time-data
Support Apple run time data in makernotes
2 parents 510b5f1 + 6d79561 commit dbc0a56

File tree

13 files changed

+475
-3
lines changed

13 files changed

+475
-3
lines changed

Diff for: Directory.Build.props

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
<ItemGroup>
4141
<PackageReference Include="CSharpIsNullAnalyzer" Version="0.1.495" PrivateAssets="all" />
4242
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23364.2" PrivateAssets="all" />
43+
<PackageReference Include="IsExternalInit" Version="1.0.3">
44+
<PrivateAssets>all</PrivateAssets>
45+
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
46+
</PackageReference>
4347
</ItemGroup>
4448

4549
<ItemGroup>

Diff for: MetadataExtractor.Tools.FileProcessor/Program.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ private static int ProcessFileList(string[] argArray)
9090
}
9191

9292
if (!markdownFormat)
93-
Console.Out.WriteLine("Processed {0:#,##0.##} MB file in {1:#,##0.##} ms\n", new FileInfo(filePath).Length/(1024d*1024), stopwatch.Elapsed.TotalMilliseconds);
93+
Console.Out.WriteLine("Processed {0:#,##0.##} MB file in {1:#,##0.##} ms\n", new FileInfo(filePath).Length / (1024d * 1024), stopwatch.Elapsed.TotalMilliseconds);
9494

9595
if (markdownFormat)
9696
{

Diff for: MetadataExtractor/Formats/Apple/BplistReader.cs

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
2+
3+
namespace MetadataExtractor.Formats.Apple;
4+
5+
/// <summary>
6+
/// A limited-functionality binary property list (BPLIST) reader.
7+
/// </summary>
8+
public sealed class BplistReader
9+
{
10+
// https://opensource.apple.com/source/CF/CF-550/ForFoundationOnly.h
11+
// https://opensource.apple.com/source/CF/CF-550/CFBinaryPList.c
12+
// https://synalysis.com/how-to-decode-apple-binary-property-list-files/
13+
14+
private static readonly byte[] _bplistHeader = { (byte)'b', (byte)'p', (byte)'l', (byte)'i', (byte)'s', (byte)'t', (byte)'0', (byte)'0' };
15+
16+
/// <summary>
17+
/// Gets whether <paramref name="bplist"/> starts with the expected header bytes.
18+
/// </summary>
19+
public static bool IsValid(byte[] bplist)
20+
{
21+
if (bplist.Length < _bplistHeader.Length)
22+
{
23+
return false;
24+
}
25+
26+
for (int i = 0; i < _bplistHeader.Length; i++)
27+
{
28+
if (bplist[i] != _bplistHeader[i])
29+
{
30+
return false;
31+
}
32+
}
33+
34+
return true;
35+
}
36+
37+
public static PropertyListResults Parse(byte[] bplist)
38+
{
39+
if (!IsValid(bplist))
40+
{
41+
throw new ArgumentException("Input is not a bplist.", nameof(bplist));
42+
}
43+
44+
Trailer trailer = ReadTrailer();
45+
46+
SequentialByteArrayReader reader = new(bplist, baseIndex: checked((int)(trailer.OffsetTableOffset + trailer.TopObject)));
47+
48+
int[] offsets = new int[(int)trailer.NumObjects];
49+
50+
for (long i = 0; i < trailer.NumObjects; i++)
51+
{
52+
if (trailer.OffsetIntSize == 1)
53+
{
54+
offsets[(int)i] = reader.GetByte();
55+
}
56+
else if (trailer.OffsetIntSize == 2)
57+
{
58+
offsets[(int)i] = reader.GetUInt16();
59+
}
60+
}
61+
62+
List<object> objects = new();
63+
64+
for (int i = 0; i < offsets.Length; i++)
65+
{
66+
reader = new SequentialByteArrayReader(bplist, offsets[i]);
67+
68+
byte b = reader.GetByte();
69+
70+
byte objectFormat = (byte)((b >> 4) & 0x0F);
71+
byte marker = (byte)(b & 0x0F);
72+
73+
object obj = objectFormat switch
74+
{
75+
// dict
76+
0x0D => HandleDict(marker),
77+
// string (ASCII)
78+
0x05 => reader.GetString(bytesRequested: marker & 0x0F, Encoding.ASCII),
79+
// data
80+
0x04 => HandleData(marker),
81+
// int
82+
0x01 => HandleInt(marker),
83+
// unknown
84+
_ => throw new NotSupportedException($"Unsupported object format {objectFormat:X2}.")
85+
};
86+
87+
objects.Add(obj);
88+
}
89+
90+
return new PropertyListResults(objects, trailer);
91+
92+
Trailer ReadTrailer()
93+
{
94+
SequentialByteArrayReader reader = new(bplist, bplist.Length - Trailer.SizeBytes);
95+
96+
// Skip 5-byte unused values, 1-byte sort version.
97+
reader.Skip(6);
98+
99+
return new Trailer
100+
{
101+
OffsetIntSize = reader.GetByte(),
102+
ObjectRefSize = reader.GetByte(),
103+
NumObjects = reader.GetInt64(),
104+
TopObject = reader.GetInt64(),
105+
OffsetTableOffset = reader.GetInt64()
106+
};
107+
}
108+
109+
object HandleInt(byte marker)
110+
{
111+
return marker switch
112+
{
113+
0 => (object)reader.GetByte(),
114+
1 => reader.GetInt16(),
115+
2 => reader.GetInt32(),
116+
3 => reader.GetInt64(),
117+
_ => throw new NotSupportedException($"Unsupported int size {marker}.")
118+
};
119+
}
120+
121+
Dictionary<byte, byte> HandleDict(byte count)
122+
{
123+
var keyRefs = new byte[count];
124+
125+
for (int j = 0; j < count; j++)
126+
{
127+
keyRefs[j] = reader.GetByte();
128+
}
129+
130+
Dictionary<byte, byte> map = new();
131+
132+
for (int j = 0; j < count; j++)
133+
{
134+
map.Add(keyRefs[j], reader.GetByte());
135+
}
136+
137+
return map;
138+
}
139+
140+
object HandleData(byte marker)
141+
{
142+
int byteCount = marker;
143+
144+
if (marker == 0x0F)
145+
{
146+
byte sizeMarker = reader.GetByte();
147+
148+
if (((sizeMarker >> 4) & 0x0F) != 1)
149+
{
150+
throw new NotSupportedException($"Invalid size marker {sizeMarker:X2}.");
151+
}
152+
153+
int sizeType = sizeMarker & 0x0F;
154+
155+
if (sizeType == 0)
156+
{
157+
byteCount = reader.GetByte();
158+
}
159+
else if (sizeType == 1)
160+
{
161+
byteCount = reader.GetUInt16();
162+
}
163+
}
164+
165+
return reader.GetBytes(byteCount);
166+
}
167+
}
168+
169+
public sealed class PropertyListResults
170+
{
171+
private readonly List<object> _objects;
172+
private readonly Trailer _trailer;
173+
174+
internal PropertyListResults(List<object> objects, Trailer trailer)
175+
{
176+
_objects = objects;
177+
_trailer = trailer;
178+
}
179+
180+
public Dictionary<byte, byte>? GetTopObject()
181+
{
182+
return _objects[checked((int)_trailer.TopObject)] as Dictionary<byte, byte>;
183+
}
184+
185+
public object Get(byte key)
186+
{
187+
return _objects[key];
188+
}
189+
}
190+
191+
internal class Trailer
192+
{
193+
public const int SizeBytes = 32;
194+
public byte OffsetIntSize { get; init; }
195+
public byte ObjectRefSize { get; init; }
196+
public long NumObjects { get; init; }
197+
public long TopObject { get; init; }
198+
public long OffsetTableOffset { get; init; }
199+
}
200+
}

Diff for: MetadataExtractor/Formats/Exif/ExifTiffHandler.cs

+10
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,16 @@ public override bool CustomProcessTag(in TiffReaderContext context, int tagId, i
210210
return true;
211211
}
212212

213+
// Custom processing for Apple RunTime tag
214+
if (tagId == AppleMakernoteDirectory.TagRunTime && CurrentDirectory is AppleMakernoteDirectory)
215+
{
216+
var bytes = context.Reader.GetBytes(valueOffset, byteCount);
217+
var directory = AppleRunTimeMakernoteDirectory.Parse(bytes);
218+
directory.Parent = CurrentDirectory;
219+
Directories.Add(directory);
220+
return true;
221+
}
222+
213223
if (HandlePrintIM(CurrentDirectory!, tagId))
214224
{
215225
var printIMDirectory = new PrintIMDirectory { Parent = CurrentDirectory };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) Drew Noakes and contributors. All Rights Reserved. Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
2+
3+
namespace MetadataExtractor.Formats.Exif.Makernotes;
4+
5+
public sealed class AppleRunTimeMakernoteDescriptor : TagDescriptor<AppleRunTimeMakernoteDirectory>
6+
{
7+
public AppleRunTimeMakernoteDescriptor(AppleRunTimeMakernoteDirectory directory) : base(directory)
8+
{
9+
}
10+
11+
public override string? GetDescription(int tagType)
12+
{
13+
return tagType switch
14+
{
15+
AppleRunTimeMakernoteDirectory.TagFlags => GetFlagsDescription(),
16+
AppleRunTimeMakernoteDirectory.TagValue => GetValueDescription(),
17+
_ => base.GetDescription(tagType),
18+
};
19+
}
20+
21+
public string? GetFlagsDescription()
22+
{
23+
// flags bitmask details
24+
// 0000 0001 = Valid
25+
// 0000 0010 = Rounded
26+
// 0000 0100 = Positive Infinity
27+
// 0000 1000 = Negative Infinity
28+
// 0001 0000 = Indefinite
29+
30+
if (Directory.TryGetInt32(AppleRunTimeMakernoteDirectory.TagFlags, out var value))
31+
{
32+
StringBuilder sb = new();
33+
34+
if ((value & 0x1) != 0)
35+
sb.Append("Valid");
36+
else
37+
sb.Append("Invalid");
38+
39+
if ((value & 0x2) != 0)
40+
sb.Append(", rounded");
41+
42+
if ((value & 0x4) != 0)
43+
sb.Append(", positive infinity");
44+
45+
if ((value & 0x8) != 0)
46+
sb.Append(", negative infinity");
47+
48+
if ((value & 0x10) != 0)
49+
sb.Append(", indefinite");
50+
51+
return sb.ToString();
52+
}
53+
54+
return base.GetDescription(AppleRunTimeMakernoteDirectory.TagFlags);
55+
}
56+
57+
public string? GetValueDescription()
58+
{
59+
if (Directory.TryGetInt64(AppleRunTimeMakernoteDirectory.TagValue, out var value) &&
60+
Directory.TryGetInt64(AppleRunTimeMakernoteDirectory.TagScale, out var scale))
61+
{
62+
return $"{value / scale} seconds";
63+
}
64+
65+
return base.GetDescription(AppleRunTimeMakernoteDirectory.TagValue);
66+
}
67+
}

0 commit comments

Comments
 (0)