Skip to content

Commit

Permalink
Implementing routines to write the Finder Tags
Browse files Browse the repository at this point in the history
  • Loading branch information
mikekazakov committed Feb 3, 2024
1 parent 1fd4382 commit 4e9b2b9
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Source/Utility/include/Utility/Tags.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <vector>
#include <string>
#include <filesystem>
#include <optional>

namespace nc::utility {

Expand Down Expand Up @@ -44,6 +45,9 @@ class Tags

// Loads tags from MDItemUserTags (1st priority) or from FinderInfo(2nd priority), works with file paths
static std::vector<Tag> ReadTags(const std::filesystem::path &_path) noexcept;

// ...
static std::vector<std::byte> BuildMDItemUserTags(std::span<const Tag> _tags) noexcept;
};

// Non-owning class that represent a text label and a color of a tag.
Expand Down
107 changes: 107 additions & 0 deletions Source/Utility/source/Tags.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <sys/xattr.h>
#include <frozen/unordered_map.h>
#include <frozen/string.h>
#include <ranges>

namespace nc::utility {

Expand Down Expand Up @@ -406,6 +407,112 @@ std::vector<Tags::Tag> Tags::ReadTags(const std::filesystem::path &_path) noexce
return tags;
}

static std::pmr::vector<std::byte> WritePListObject(const Tags::Tag &_tag, std::pmr::memory_resource &_mem) noexcept
{
std::pmr::vector<std::byte> dst(&_mem);
const std::string &label = _tag.Label();
const bool is_ascii = std::ranges::all_of(label, [](auto _c) { return static_cast<unsigned char>(_c) < 0x7F; });
if( is_ascii ) {
const size_t len_color = _tag.Color() == Tags::Color::None ? 0 : 2;
const size_t len = label.length() + len_color;
if( len < 15 ) {
// write the byte marker
dst.push_back(std::byte{static_cast<unsigned char>(0x50 + len)});
// write the label
dst.insert(dst.end(),
reinterpret_cast<const std::byte *>(label.data()),
reinterpret_cast<const std::byte *>(label.data() + label.length()));
if( len_color != 0 ) {
// write the color if it's not None
dst.push_back(std::byte{'\x0a'});
dst.push_back(std::byte{static_cast<unsigned char>('0' + std::to_underlying(_tag.Color()))});
}
return dst;
}
else {
abort(); // TODO: implement
}
}
else {
abort(); // TODO: implement
}
}

std::vector<std::byte> Tags::BuildMDItemUserTags(const std::span<const Tag> _tags) noexcept
{
if( _tags.empty() )
return {};

std::array<char, 4096> mem_buffer;
std::pmr::monotonic_buffer_resource mem_resource(mem_buffer.data(), mem_buffer.size());

// Build serialized representation of the tags
std::pmr::vector<std::pmr::vector<std::byte>> objects(&mem_resource);
for( auto &tag : _tags ) {
objects.emplace_back(WritePListObject(tag, mem_resource));
}

if( objects.size() > 14 ) {
// for now the algorithm is simpified to support only up to 14 tags simultaneously, which will be enough unless
// the system is abused.
objects.resize(14);
}

std::pmr::vector<size_t> offsets; // offset of every object written into the plist will be gathered here

// Write the magick prologue
std::pmr::vector<std::byte> plist;
plist.insert(plist.end(),
reinterpret_cast<const std::byte *>(g_Prologue.data()),
reinterpret_cast<const std::byte *>(g_Prologue.data() + g_Prologue.length()));

// Write an array object with up to 14 objects
offsets.push_back(plist.size());
plist.push_back(std::byte{static_cast<unsigned char>(0xA0 + objects.size())});

// Write the object references
for( size_t i = 0; i < objects.size(); ++i )
plist.push_back(std::byte{static_cast<unsigned char>(i + 1)});

// Write the objects themselves
for( auto &object : objects ) {
offsets.push_back(plist.size());
plist.insert(plist.end(), object.begin(), object.end());
}

// Deduce the stride of the offset table
size_t offset_int_size = 1;
if( const size_t max = *std::max_element(offsets.begin(), offsets.end()); max > 255 ) {
abort(); // TODO: implement
}

// Compose the trailer to be written later on
Trailer trailer;
memset(&trailer, 0, sizeof(trailer));
trailer.offset_int_size = static_cast<uint8_t>(offset_int_size);
trailer.object_ref_size = 1;
trailer.num_objects = std::byteswap(static_cast<uint64_t>(objects.size() + 1));
trailer.offset_table_offset = std::byteswap(static_cast<uint64_t>(plist.size()));

// Write the offset table
for( const size_t offset : offsets ) {
if( offset_int_size == 1 ) {
plist.push_back(std::byte{static_cast<unsigned char>(offset)});
}
else {
abort(); // TODO: implement
}
}

// Write the trailer
plist.insert(plist.end(),
reinterpret_cast<const std::byte *>(&trailer),
reinterpret_cast<const std::byte *>(&trailer) + sizeof(trailer));

// Done.
return {plist.begin(), plist.end()};
}

Tags::Tag::Tag(const std::string *const _label, const Tags::Color _color) noexcept
: m_TaggedPtr{reinterpret_cast<const std::string *>(reinterpret_cast<uint64_t>(_label) |
static_cast<uint64_t>(std::to_underlying(_color)))}
Expand Down
67 changes: 67 additions & 0 deletions Source/Utility/tests/Tags_UT.mm
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,70 @@
unlink(path.c_str());
}
}

TEST_CASE(PREFIX "BuildMDItemUserTags")
{
std::set<std::string> labels;
auto tag = [&labels](const char *_l, Tags::Color _c) { return Tags::Tag(&*labels.emplace(_l).first, _c); };

struct TC {
std::vector<Tags::Tag> labels;
std::vector<unsigned char> expected_bytes;
} const tcs[] = {
{{}, {}},
{{tag("None", Tags::Color::None)},
{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x54, 0x4e, 0x6f, 0x6e, 0x65, 0x08, 0x0a,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f}},
{{tag("Gray", Tags::Color::Gray)},
{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x56, 0x47, 0x72, 0x61, 0x79, 0x0a, 0x31,
0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11}},
{{tag("Green", Tags::Color::Green)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x57, 0x47, 0x72, 0x65, 0x65, 0x6e, 0x0a, 0x32,
0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12}}},
{{tag("Purple", Tags::Color::Purple)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x58, 0x50, 0x75, 0x72, 0x70, 0x6c, 0x65, 0x0a,
0x33, 0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13}}},
{{tag("Blue", Tags::Color::Blue)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x56, 0x42, 0x6c, 0x75, 0x65, 0x0a, 0x34,
0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11}}},
{{tag("Yellow", Tags::Color::Yellow)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x58, 0x59, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x0a,
0x35, 0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13}}},
{{tag("Red", Tags::Color::Red)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x55, 0x52, 0x65, 0x64, 0x0a, 0x36, 0x08,
0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10}}},
{{tag("Orange", Tags::Color::Orange)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x58, 0x4f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x0a,
0x37, 0x08, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13}}},
{{tag("Blue", Tags::Color::Blue),
tag("Grey", Tags::Color::Gray),
tag("Green", Tags::Color::Green),
tag("Orange", Tags::Color::Orange),
tag("Purple", Tags::Color::Purple),
tag("Red", Tags::Color::Red),
tag("Yellow", Tags::Color::Yellow),
tag("Home", Tags::Color::None)},
{{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa8, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x56, 0x42, 0x6c, 0x75, 0x65, 0x0a, 0x34, 0x56, 0x47, 0x72, 0x65, 0x79, 0x0a, 0x31, 0x57, 0x47, 0x72,
0x65, 0x65, 0x6e, 0x0a, 0x32, 0x58, 0x4f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x0a, 0x37, 0x58, 0x50, 0x75,
0x72, 0x70, 0x6c, 0x65, 0x0a, 0x33, 0x55, 0x52, 0x65, 0x64, 0x0a, 0x36, 0x58, 0x59, 0x65, 0x6c, 0x6c,
0x6f, 0x77, 0x0a, 0x35, 0x54, 0x48, 0x6f, 0x6d, 0x65, 0x08, 0x11, 0x18, 0x1f, 0x27, 0x30, 0x39, 0x3f,
0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4d}}},
};

for( const auto &tc : tcs ) {
auto bytes = Tags::BuildMDItemUserTags(tc.labels);
CHECK(tc.expected_bytes ==
std::vector<unsigned char>{reinterpret_cast<const unsigned char *>(bytes.data()),
reinterpret_cast<const unsigned char *>(bytes.data() + bytes.size())});
}
}

0 comments on commit 4e9b2b9

Please sign in to comment.