-
Notifications
You must be signed in to change notification settings - Fork 170
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support Apple run time makernote data
This data is encoded as a bplist (binary property list). We add a `BplistReader` class with some basic support for this format -- enough to obtain what we need from the RunTime makernote data. Ported from the Java implementation.
- Loading branch information
1 parent
8a0edbc
commit e6d4f72
Showing
9 changed files
with
461 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,200 @@ | ||
// 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. | ||
|
||
namespace MetadataExtractor.Formats.Apple; | ||
|
||
/// <summary> | ||
/// A limited-functionality binary property list (BPLIST) reader. | ||
/// </summary> | ||
public sealed class BplistReader | ||
{ | ||
// https://opensource.apple.com/source/CF/CF-550/ForFoundationOnly.h | ||
// https://opensource.apple.com/source/CF/CF-550/CFBinaryPList.c | ||
// https://synalysis.com/how-to-decode-apple-binary-property-list-files/ | ||
|
||
private static readonly byte[] _bplistHeader = { (byte)'b', (byte)'p', (byte)'l', (byte)'i', (byte)'s', (byte)'t', (byte)'0', (byte)'0' }; | ||
|
||
/// <summary> | ||
/// Gets whether <paramref name="bplist"/> starts with the expected header bytes. | ||
/// </summary> | ||
public static bool IsValid(byte[] bplist) | ||
{ | ||
if (bplist.Length < _bplistHeader.Length) | ||
{ | ||
return false; | ||
} | ||
|
||
for (int i = 0; i < _bplistHeader.Length; i++) | ||
{ | ||
if (bplist[i] != _bplistHeader[i]) | ||
{ | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public static PropertyListResults Parse(byte[] bplist) | ||
{ | ||
if (!IsValid(bplist)) | ||
{ | ||
throw new ArgumentException("Input is not a bplist.", nameof(bplist)); | ||
} | ||
|
||
Trailer trailer = ReadTrailer(); | ||
|
||
SequentialByteArrayReader reader = new(bplist, baseIndex: checked((int)(trailer.OffsetTableOffset + trailer.TopObject))); | ||
|
||
int[] offsets = new int[(int)trailer.NumObjects]; | ||
|
||
for (long i = 0; i < trailer.NumObjects; i++) | ||
{ | ||
if (trailer.OffsetIntSize == 1) | ||
{ | ||
offsets[(int)i] = reader.GetByte(); | ||
} | ||
else if (trailer.OffsetIntSize == 2) | ||
{ | ||
offsets[(int)i] = reader.GetUInt16(); | ||
} | ||
} | ||
|
||
List<object> objects = new(); | ||
|
||
for (int i = 0; i < offsets.Length; i++) | ||
{ | ||
reader = new SequentialByteArrayReader(bplist, offsets[i]); | ||
|
||
byte b = reader.GetByte(); | ||
|
||
byte objectFormat = (byte)((b >> 4) & 0x0F); | ||
byte marker = (byte)(b & 0x0F); | ||
|
||
object obj = objectFormat switch | ||
{ | ||
// dict | ||
0x0D => HandleDict(marker), | ||
// string (ASCII) | ||
0x05 => reader.GetString(bytesRequested: marker & 0x0F, Encoding.ASCII), | ||
// data | ||
0x04 => HandleData(marker), | ||
// int | ||
0x01 => HandleInt(marker), | ||
// unknown | ||
_ => throw new NotSupportedException($"Unsupported object format {objectFormat:X2}.") | ||
}; | ||
|
||
objects.Add(obj); | ||
} | ||
|
||
return new PropertyListResults(objects, trailer); | ||
|
||
Trailer ReadTrailer() | ||
{ | ||
SequentialByteArrayReader reader = new(bplist, bplist.Length - Trailer.SizeBytes); | ||
|
||
// Skip 5-byte unused values, 1-byte sort version. | ||
reader.Skip(6); | ||
|
||
return new Trailer | ||
{ | ||
OffsetIntSize = reader.GetByte(), | ||
ObjectRefSize = reader.GetByte(), | ||
NumObjects = reader.GetInt64(), | ||
TopObject = reader.GetInt64(), | ||
OffsetTableOffset = reader.GetInt64() | ||
}; | ||
} | ||
|
||
object HandleInt(byte marker) | ||
{ | ||
return marker switch | ||
{ | ||
0 => (object)reader.GetByte(), | ||
1 => reader.GetInt16(), | ||
2 => reader.GetInt32(), | ||
3 => reader.GetInt64(), | ||
_ => throw new NotSupportedException($"Unsupported int size {marker}.") | ||
}; | ||
} | ||
|
||
Dictionary<byte, byte> HandleDict(byte count) | ||
{ | ||
var keyRefs = new byte[count]; | ||
|
||
for (int j = 0; j < count; j++) | ||
{ | ||
keyRefs[j] = reader.GetByte(); | ||
} | ||
|
||
Dictionary<byte, byte> map = new(); | ||
|
||
for (int j = 0; j < count; j++) | ||
{ | ||
map.Add(keyRefs[j], reader.GetByte()); | ||
} | ||
|
||
return map; | ||
} | ||
|
||
object HandleData(byte marker) | ||
{ | ||
int byteCount = marker; | ||
|
||
if (marker == 0x0F) | ||
{ | ||
byte sizeMarker = reader.GetByte(); | ||
|
||
if (((sizeMarker >> 4) & 0x0F) != 1) | ||
{ | ||
throw new NotSupportedException($"Invalid size marker {sizeMarker:X2}."); | ||
} | ||
|
||
int sizeType = sizeMarker & 0x0F; | ||
|
||
if (sizeType == 0) | ||
{ | ||
byteCount = reader.GetByte(); | ||
} | ||
else if (sizeType == 1) | ||
{ | ||
byteCount = reader.GetUInt16(); | ||
} | ||
} | ||
|
||
return reader.GetBytes(byteCount); | ||
} | ||
} | ||
|
||
public sealed class PropertyListResults | ||
{ | ||
private readonly List<object> _objects; | ||
private readonly Trailer _trailer; | ||
|
||
internal PropertyListResults(List<object> objects, Trailer trailer) | ||
{ | ||
_objects = objects; | ||
_trailer = trailer; | ||
} | ||
|
||
public Dictionary<byte, byte>? GetTopObject() | ||
{ | ||
return _objects[checked((int)_trailer.TopObject)] as Dictionary<byte, byte>; | ||
} | ||
|
||
public object Get(byte key) | ||
{ | ||
return _objects[key]; | ||
} | ||
} | ||
|
||
internal class Trailer | ||
{ | ||
public const int SizeBytes = 32; | ||
public byte OffsetIntSize { get; init; } | ||
public byte ObjectRefSize { get; init; } | ||
public long NumObjects { get; init; } | ||
public long TopObject { get; init; } | ||
public long OffsetTableOffset { get; init; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
MetadataExtractor/Formats/Exif/Makernotes/AppleRunTimeMakernoteDescriptor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// 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. | ||
|
||
namespace MetadataExtractor.Formats.Exif.Makernotes; | ||
|
||
public sealed class AppleRunTimeMakernoteDescriptor : TagDescriptor<AppleRunTimeMakernoteDirectory> | ||
{ | ||
public AppleRunTimeMakernoteDescriptor(AppleRunTimeMakernoteDirectory directory) : base(directory) | ||
{ | ||
} | ||
|
||
public override string? GetDescription(int tagType) | ||
{ | ||
return tagType switch | ||
{ | ||
AppleRunTimeMakernoteDirectory.TagFlags => GetFlagsDescription(), | ||
AppleRunTimeMakernoteDirectory.TagValue => GetValueDescription(), | ||
_ => base.GetDescription(tagType), | ||
}; | ||
} | ||
|
||
public string? GetFlagsDescription() | ||
{ | ||
// flags bitmask details | ||
// 0000 0001 = Valid | ||
// 0000 0010 = Rounded | ||
// 0000 0100 = Positive Infinity | ||
// 0000 1000 = Negative Infinity | ||
// 0001 0000 = Indefinite | ||
|
||
if (Directory.TryGetInt32(AppleRunTimeMakernoteDirectory.TagFlags, out var value)) | ||
{ | ||
StringBuilder sb = new(); | ||
|
||
if ((value & 0x1) != 0) | ||
sb.Append("Valid"); | ||
else | ||
sb.Append("Invalid"); | ||
|
||
if ((value & 0x2) != 0) | ||
sb.Append(", rounded"); | ||
|
||
if ((value & 0x4) != 0) | ||
sb.Append(", positive infinity"); | ||
|
||
if ((value & 0x8) != 0) | ||
sb.Append(", negative infinity"); | ||
|
||
if ((value & 0x10) != 0) | ||
sb.Append(", indefinite"); | ||
|
||
return sb.ToString(); | ||
} | ||
|
||
return base.GetDescription(AppleRunTimeMakernoteDirectory.TagFlags); | ||
} | ||
|
||
public string? GetValueDescription() | ||
{ | ||
if (Directory.TryGetInt64(AppleRunTimeMakernoteDirectory.TagValue, out var value) && | ||
Directory.TryGetInt64(AppleRunTimeMakernoteDirectory.TagScale, out var scale)) | ||
{ | ||
return $"{value / scale} seconds"; | ||
} | ||
|
||
return base.GetDescription(AppleRunTimeMakernoteDirectory.TagValue); | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
MetadataExtractor/Formats/Exif/Makernotes/AppleRunTimeMakernoteDirectory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// 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. | ||
|
||
using MetadataExtractor.Formats.Apple; | ||
|
||
namespace MetadataExtractor.Formats.Exif.Makernotes; | ||
|
||
public sealed class AppleRunTimeMakernoteDirectory : Directory | ||
{ | ||
public const int TagFlags = 1; | ||
public const int TagEpoch = 2; | ||
public const int TagScale = 3; | ||
public const int TagValue = 4; | ||
|
||
private static readonly Dictionary<int, string> _tagNameMap = new(); | ||
|
||
static AppleRunTimeMakernoteDirectory() | ||
{ | ||
_tagNameMap[TagFlags] = "Flags"; | ||
_tagNameMap[TagEpoch] = "Epoch"; | ||
_tagNameMap[TagScale] = "Scale"; | ||
_tagNameMap[TagValue] = "Value"; | ||
} | ||
|
||
public AppleRunTimeMakernoteDirectory() : base(_tagNameMap) | ||
{ | ||
SetDescriptor(new AppleRunTimeMakernoteDescriptor(this)); | ||
} | ||
|
||
public override string Name => "Apple Run Time"; | ||
|
||
public static AppleRunTimeMakernoteDirectory Parse(byte[] bytes) | ||
{ | ||
AppleRunTimeMakernoteDirectory directory = new(); | ||
|
||
if (!BplistReader.IsValid(bytes)) | ||
{ | ||
directory.AddError("Input array is not a bplist."); | ||
} | ||
else | ||
{ | ||
try | ||
{ | ||
ProcessAppleRunTime(); | ||
} | ||
catch (IOException ex) | ||
{ | ||
directory.AddError($"Error processing {nameof(AppleRunTimeMakernoteDirectory)}: {ex.Message}"); | ||
} | ||
} | ||
|
||
return directory; | ||
|
||
void ProcessAppleRunTime() | ||
{ | ||
var results = BplistReader.Parse(bytes); | ||
|
||
var entrySet = results.GetTopObject(); | ||
|
||
if (entrySet is not null) | ||
{ | ||
Dictionary<string, object> values = new(entrySet.Count); | ||
|
||
foreach (var pair in entrySet) | ||
{ | ||
var key = (string)results.Get(pair.Key); | ||
var value = results.Get(pair.Value); | ||
|
||
values[key] = value; | ||
} | ||
|
||
// https://developer.apple.com/documentation/coremedia/cmtime-u58 | ||
|
||
if (values.TryGetValue("flags", out var flagsObject)) | ||
{ | ||
if (flagsObject is byte flags) | ||
{ | ||
if ((flags & 0x1) == 0x1) | ||
{ | ||
directory.Set(TagFlags, flags); | ||
directory.Set(TagEpoch, (byte)values["epoch"]); | ||
directory.Set(TagScale, (int)values["timescale"]); | ||
directory.Set(TagValue, (long)values["value"]); | ||
} | ||
} | ||
else if (flagsObject is string flagsString) | ||
{ | ||
var parsedFlags = byte.Parse(flagsString); | ||
if ((parsedFlags & 0x1) == 0x1) | ||
{ | ||
directory.Set(TagFlags, parsedFlags); | ||
directory.Set(TagEpoch, byte.Parse((string)values["epoch"])); | ||
directory.Set(TagScale, int.Parse((string)values["timescale"])); | ||
directory.Set(TagValue, long.Parse((string)values["value"])); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.