Skip to content

Commit

Permalink
Support Apple run time makernote data
Browse files Browse the repository at this point in the history
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
drewnoakes committed Aug 28, 2023
1 parent 8a0edbc commit e6d4f72
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
<ItemGroup>
<PackageReference Include="CSharpIsNullAnalyzer" Version="0.1.495" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0-beta1.23364.2" PrivateAssets="all" />
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
200 changes: 200 additions & 0 deletions MetadataExtractor/Formats/Apple/BplistReader.cs
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; }
}
}
10 changes: 10 additions & 0 deletions MetadataExtractor/Formats/Exif/ExifTiffHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ public override bool CustomProcessTag(in TiffReaderContext context, int tagId, i
return true;
}

// Custom processing for Apple RunTime tag
if (tagId == AppleMakernoteDirectory.TagRunTime && CurrentDirectory is AppleMakernoteDirectory)
{
var bytes = context.Reader.GetBytes(valueOffset, byteCount);
var directory = AppleRunTimeMakernoteDirectory.Parse(bytes);
directory.Parent = CurrentDirectory;
Directories.Add(directory);
return true;
}

if (HandlePrintIM(CurrentDirectory!, tagId))
{
var printIMDirectory = new PrintIMDirectory { Parent = CurrentDirectory };
Expand Down
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);
}
}
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"]));
}
}
}
}
}
}
}
Loading

0 comments on commit e6d4f72

Please sign in to comment.