diff --git a/Source/Utility/source/Tags.cpp b/Source/Utility/source/Tags.cpp index 5f2520719..9896bdf43 100644 --- a/Source/Utility/source/Tags.cpp +++ b/Source/Utility/source/Tags.cpp @@ -29,6 +29,324 @@ static constexpr const char *g_FinderInfo = "com.apple.FinderInfo"; [[clang::no_destroy]] static const std::string g_LabelRed = "Red"; [[clang::no_destroy]] static const std::string g_LabelOrange = "Orange"; +// Run this script to rebuild the table: +// for i in {0..7}; do key="TG_COLOR_$i"; find /System/Library/CoreServices/Finder.app/Contents/Resources -name +// "Localizable.strings" -exec /usr/libexec/PlistBuddy -c "Print :$key" {} \; | sed -n "s/\(.*\)/{\"&\", $i},/p"; done +[[clang::no_destroy]] static const robin_hood::unordered_flat_map g_LocalizedToColors{ + {"Ohne Farbe", 0}, + {"ללא צבע", 0}, + {"No Colour", 0}, + {"لا يوجد لون", 0}, + {"Κανένα χρώμα", 0}, + {"カラーなし", 0}, + {"No Color", 0}, + {"Немає кольору", 0}, + {"Sin color", 0}, + {"无颜色", 0}, + {"Sin color", 0}, + {"Nenhuma Cor", 0}, + {"Ingen farve", 0}, + {"Nessun colore", 0}, + {"Žiadna farba", 0}, + {"Nenhuma cor", 0}, + {"Tiada Warna", 0}, + {"Ingen färg", 0}, + {"Žádná barva", 0}, + {"색상 없음", 0}, + {"Ingen farge", 0}, + {"Nincs szín", 0}, + {"沒有顏色", 0}, + {"Renk Yok", 0}, + {"Brak koloru", 0}, + {"沒有顏色", 0}, + {"No Colour", 0}, + {"Không có màu", 0}, + {"Без цвета", 0}, + {"Aucune couleur", 0}, + {"Aucune couleur", 0}, + {"Ei väriä", 0}, + {"Tidak Ada Warna", 0}, + {"Geen kleur", 0}, + {"ไม่มีสี", 0}, + {"Nicio culoare", 0}, + {"Bez boje", 0}, + {"कोई रंग नहीं", 0}, + {"Cap color", 0}, + {"Grau", 1}, + {"אפור", 1}, + {"Grey", 1}, + {"رمادي", 1}, + {"Γκρι", 1}, + {"グレイ", 1}, + {"Gray", 1}, + {"Сірий", 1}, + {"Gris", 1}, + {"灰色", 1}, + {"Gris", 1}, + {"Cinza", 1}, + {"Grå", 1}, + {"Grigio", 1}, + {"Sivá", 1}, + {"Cinzento", 1}, + {"Kelabu", 1}, + {"Grå", 1}, + {"Šedá", 1}, + {"회색", 1}, + {"Grå", 1}, + {"Szürke", 1}, + {"灰色", 1}, + {"Gri", 1}, + {"Szary", 1}, + {"灰色", 1}, + {"Grey", 1}, + {"Xám", 1}, + {"Серый", 1}, + {"Gris", 1}, + {"Gris", 1}, + {"Harmaa", 1}, + {"Abu-Abu", 1}, + {"Grijs", 1}, + {"สีเทา", 1}, + {"Gri", 1}, + {"Siva", 1}, + {"धूसर", 1}, + {"Grisa", 1}, + {"Grün", 2}, + {"ירוק", 2}, + {"Green", 2}, + {"أخضر", 2}, + {"Πράσινο", 2}, + {"グリーン", 2}, + {"Green", 2}, + {"Зелений", 2}, + {"Verde", 2}, + {"绿色", 2}, + {"Verde", 2}, + {"Verde", 2}, + {"Grøn", 2}, + {"Verde", 2}, + {"Zelená", 2}, + {"Verde", 2}, + {"Hijau", 2}, + {"Grön", 2}, + {"Zelená", 2}, + {"초록색", 2}, + {"Grønn", 2}, + {"Zöld", 2}, + {"綠色", 2}, + {"Yeşil", 2}, + {"Zielony", 2}, + {"綠色", 2}, + {"Green", 2}, + {"Lục", 2}, + {"Зеленый", 2}, + {"Vert", 2}, + {"Vert", 2}, + {"Vihreä", 2}, + {"Hijau", 2}, + {"Groen", 2}, + {"สีเขียว", 2}, + {"Verde", 2}, + {"Zelena", 2}, + {"हरा", 2}, + {"Verda", 2}, + {"Lila", 3}, + {"סגול", 3}, + {"Purple", 3}, + {"أرجواني", 3}, + {"Μοβ", 3}, + {"パープル", 3}, + {"Purple", 3}, + {"Бузковий", 3}, + {"Morado", 3}, + {"紫色", 3}, + {"Morado", 3}, + {"Roxo", 3}, + {"Lilla", 3}, + {"Viola", 3}, + {"Fialová", 3}, + {"Roxo", 3}, + {"Ungu", 3}, + {"Lila", 3}, + {"Fialová", 3}, + {"보라색", 3}, + {"Lilla", 3}, + {"Bíbor", 3}, + {"紫色", 3}, + {"Mor", 3}, + {"Purpurowy", 3}, + {"紫色", 3}, + {"Purple", 3}, + {"Tía", 3}, + {"Лиловый", 3}, + {"Violet", 3}, + {"Violet", 3}, + {"Violetti", 3}, + {"Ungu", 3}, + {"Paars", 3}, + {"สีม่วง", 3}, + {"Mov", 3}, + {"Ljubičasta", 3}, + {"जामुनी", 3}, + {"Morada", 3}, + {"Blau", 4}, + {"כחול", 4}, + {"Blue", 4}, + {"أزرق", 4}, + {"Μπλε", 4}, + {"ブルー", 4}, + {"Blue", 4}, + {"Синій", 4}, + {"Azul", 4}, + {"蓝色", 4}, + {"Azul", 4}, + {"Azul", 4}, + {"Blå", 4}, + {"Blu", 4}, + {"Modrá", 4}, + {"Azul", 4}, + {"Biru", 4}, + {"Blå", 4}, + {"Modrá", 4}, + {"파란색", 4}, + {"Blå", 4}, + {"Kék", 4}, + {"藍色", 4}, + {"Mavi", 4}, + {"Niebieski", 4}, + {"藍色", 4}, + {"Blue", 4}, + {"Lam", 4}, + {"Синий", 4}, + {"Bleu", 4}, + {"Bleu", 4}, + {"Sininen", 4}, + {"Biru", 4}, + {"Blauw", 4}, + {"สีน้ำเงิน", 4}, + {"Albastru", 4}, + {"Plava", 4}, + {"नीला", 4}, + {"Blava", 4}, + {"Gelb", 5}, + {"צהוב", 5}, + {"Yellow", 5}, + {"أصفر", 5}, + {"Κίτρινο", 5}, + {"イエロー", 5}, + {"Yellow", 5}, + {"Жовтий", 5}, + {"Amarillo", 5}, + {"黄色", 5}, + {"Amarillo", 5}, + {"Amarelo", 5}, + {"Gul", 5}, + {"Giallo", 5}, + {"Žltá", 5}, + {"Amarelo", 5}, + {"Kuning", 5}, + {"Gul", 5}, + {"Žlutá", 5}, + {"노란색", 5}, + {"Gul", 5}, + {"Sárga", 5}, + {"黃色", 5}, + {"Sarı", 5}, + {"Żółty", 5}, + {"黃色", 5}, + {"Yellow", 5}, + {"Vàng", 5}, + {"Желтый", 5}, + {"Jaune", 5}, + {"Jaune", 5}, + {"Keltainen", 5}, + {"Kuning", 5}, + {"Geel", 5}, + {"สีเหลือง", 5}, + {"Galben", 5}, + {"Žuta", 5}, + {"पीला", 5}, + {"Groga", 5}, + {"Rot", 6}, + {"אדום", 6}, + {"Red", 6}, + {"أحمر", 6}, + {"Κόκκινο", 6}, + {"レッド", 6}, + {"Red", 6}, + {"Червоний", 6}, + {"Rojo", 6}, + {"红色", 6}, + {"Rojo", 6}, + {"Vermelho", 6}, + {"Rød", 6}, + {"Rosso", 6}, + {"Červená", 6}, + {"Vermelho", 6}, + {"Merah", 6}, + {"Röd", 6}, + {"Červená", 6}, + {"빨간색", 6}, + {"Rød", 6}, + {"Piros", 6}, + {"紅色", 6}, + {"Kırmızı", 6}, + {"Czerwony", 6}, + {"紅色", 6}, + {"Red", 6}, + {"Đỏ", 6}, + {"Красный", 6}, + {"Rouge", 6}, + {"Rouge", 6}, + {"Punainen", 6}, + {"Merah", 6}, + {"Rood", 6}, + {"สีแดง", 6}, + {"Roșu", 6}, + {"Crvena", 6}, + {"लाल", 6}, + {"Vermella", 6}, + {"Orange", 7}, + {"כתום", 7}, + {"Orange", 7}, + {"برتقالي", 7}, + {"Πορτοκαλί", 7}, + {"オレンジ", 7}, + {"Orange", 7}, + {"Оранжевий", 7}, + {"Naranja", 7}, + {"橙色", 7}, + {"Naranja", 7}, + {"Laranja", 7}, + {"Orange", 7}, + {"Arancione", 7}, + {"Oranžová", 7}, + {"Laranja", 7}, + {"Jingga", 7}, + {"Orange", 7}, + {"Oranžová", 7}, + {"주황색", 7}, + {"Oransje", 7}, + {"Narancs", 7}, + {"橙色", 7}, + {"Turuncu", 7}, + {"Pomarańczowy", 7}, + {"橙色", 7}, + {"Orange", 7}, + {"Cam", 7}, + {"Оранжевый", 7}, + {"Orange", 7}, + {"Orange", 7}, + {"Oranssi", 7}, + {"Oranye", 7}, + {"Oranje", 7}, + {"สีส้ม", 7}, + {"Portocaliu", 7}, + {"Narančasta", 7}, + {"नारंगी", 7}, + {"Taronja", 7}, +}; + namespace { struct Trailer { @@ -108,7 +426,7 @@ static std::optional ParseTag(std::u16string_view _tag_rep) noexcept if( _tag_rep.empty() ) return {}; - Tags::Color color = Tags::Color::None; + std::optional color; if( _tag_rep.size() >= 3 && // std::byteswap(_tag_rep[_tag_rep.size() - 2]) == '\x0a' && // std::byteswap(_tag_rep[_tag_rep.size() - 1]) >= '0' && // @@ -125,19 +443,34 @@ static std::optional ParseTag(std::u16string_view _tag_rep) noexcept kCFStringEncodingUTF16BE, false, kCFAllocatorNull)); - if( cf_str ) { - if( const char *cstr = CFStringGetCStringPtr(cf_str.get(), kCFStringEncodingUTF8) ) - return Tags::Tag{InternalizeString(cstr), color}; + if( !cf_str ) + return {}; + const std::string *label = nullptr; + if( const char *cstr = CFStringGetCStringPtr(cf_str.get(), kCFStringEncodingUTF8) ) { + label = InternalizeString(cstr); + } + else { const CFIndex length = CFStringGetLength(cf_str.get()); const CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; std::array mem_buffer; std::pmr::monotonic_buffer_resource mem_resource(mem_buffer.data(), mem_buffer.size()); std::pmr::vector str_buf(max_size, &mem_resource); if( CFStringGetCString(cf_str.get(), str_buf.data(), max_size, kCFStringEncodingUTF8) ) - return Tags::Tag{InternalizeString(str_buf.data()), color}; + label = InternalizeString(str_buf.data()); } - return {}; + + if( label == nullptr ) + return {}; + + if( color == std::nullopt ) { + // Old versions of MacOS can write only a label without the tag color index, relying on the value of the label + // to deduce the color. That's for "base" colors. In these cases - try to do the same. + auto it = g_LocalizedToColors.find(*label); + color = it == g_LocalizedToColors.end() ? Tags::Color::None : Tags::Color{it->second}; + } + + return Tags::Tag{label, *color}; } namespace { @@ -272,7 +605,7 @@ std::vector Tags::ReadTags(int _fd) noexcept const ssize_t res = flistxattr(_fd, buf.data(), buf.size(), 0); if( res <= 0 ) return {}; - + // 1st - try MDItemUserTags const bool has_usertags = memmem(buf.data(), res, g_MDItemUserTags, std::string_view{g_MDItemUserTags}.length()) != nullptr; diff --git a/Source/Utility/tests/Tags_UT.mm b/Source/Utility/tests/Tags_UT.mm index a591e7751..b0f69d1e3 100644 --- a/Source/Utility/tests/Tags_UT.mm +++ b/Source/Utility/tests/Tags_UT.mm @@ -162,6 +162,48 @@ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21}, "Привет!!!", Tags::Color::Blue}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x67, 0x04, 0x21, 0x04, 0x38, + 0x04, 0x3d, 0x04, 0x38, 0x04, 0x39, 0x00, 0x0a, 0x00, 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, 0x19}, + "Синий", + Tags::Color::Blue}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x65, 0x04, 0x21, 0x04, + 0x35, 0x04, 0x40, 0x04, 0x4b, 0x04, 0x39, 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, 0x15}, + "Серый", + Tags::Color::Gray}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x69, 0x04, 0x17, 0x04, 0x35, 0x04, + 0x3b, 0x04, 0x35, 0x04, 0x3d, 0x04, 0x4b, 0x04, 0x39, 0x00, 0x0a, 0x00, 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, 0x1d}, + "Зеленый", + Tags::Color::Green}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x69, 0x04, 0x1e, 0x04, 0x40, 0x04, + 0x30, 0x04, 0x3d, 0x04, 0x36, 0x04, 0x35, 0x04, 0x32, 0x04, 0x4b, 0x04, 0x39, 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, 0x1d}, + "Оранжевый", + Tags::Color::Orange}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x67, 0x04, 0x1b, 0x04, 0x38, + 0x04, 0x3b, 0x04, 0x3e, 0x04, 0x32, 0x04, 0x4b, 0x04, 0x39, 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, 0x19}, + "Лиловый", + Tags::Color::Purple}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x67, 0x04, 0x1a, 0x04, 0x40, + 0x04, 0x30, 0x04, 0x41, 0x04, 0x3d, 0x04, 0x4b, 0x04, 0x39, 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, 0x19}, + "Красный", + Tags::Color::Red}, + {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x66, 0x04, 0x16, 0x04, 0x35, + 0x04, 0x3b, 0x04, 0x42, 0x04, 0x4b, 0x04, 0x39, 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, 0x17}, + "Желтый", + Tags::Color::Yellow}, {{0x62, 0x70, 0x6c, 0x69, 0x73, 0x74, 0x30, 0x30, 0xa1, 0x01, 0x6f, 0x10, 0x52, 0x04, 0x2d, 0x04, 0x42, 0x04, 0x3e, 0x00, 0x20, 0x04, 0x3e, 0x04, 0x47, 0x04, 0x35, 0x04, 0x3d, 0x04, 0x4c, 0x00, 0x20, 0x04, 0x34, 0x04, 0x3b, 0x04, 0x38, 0x04, 0x3d, 0x04, 0x3d, 0x04, 0x3e, 0x04, 0x35, 0x00, 0x20, 0x04, 0x38, 0x04, 0x3c, 0x04, @@ -184,6 +226,7 @@ Tags::Color::None}, }; for( auto &tc : tcs ) { + INFO(fmt::format("{} - {}", tc.expected_label, std::to_underlying(tc.expected_color))); auto tags = Tags::ParseMDItemUserTags({reinterpret_cast(tc.bytes.data()), tc.bytes.size()}); REQUIRE(tags.size() == 1); CHECK(tags[0] == Tags::Tag(&tc.expected_label, tc.expected_color));