Skip to content

Commit

Permalink
Merge pull request #343 from drewnoakes/apple-run-time-data
Browse files Browse the repository at this point in the history
Support Apple run time data in makernotes
  • Loading branch information
drewnoakes authored Aug 28, 2023
2 parents 510b5f1 + 6d79561 commit dbc0a56
Show file tree
Hide file tree
Showing 13 changed files with 475 additions and 3 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
2 changes: 1 addition & 1 deletion MetadataExtractor.Tools.FileProcessor/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ private static int ProcessFileList(string[] argArray)
}

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

if (markdownFormat)
{
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);
}
}
Loading

0 comments on commit dbc0a56

Please sign in to comment.