18eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary/*
28eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * Copyright 2011 Google Inc.
38eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary *
48eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * Use of this source code is governed by a BSD-style license that can be
58eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * found in the LICENSE file.
68eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary */
78eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
88eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary#include "SkPDFConvertType1FontStream.h"
94d1955c43aaab045511b74a495dfbea4ef0057c5Ben Wagner#include "SkTemplates.h"
108eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
118eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary#include <ctype.h>
128eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
138eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanarystatic bool parsePFBSection(const uint8_t** src, size_t* len, int sectionType,
148eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary                            size_t* size) {
158eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // PFB sections have a two or six bytes header. 0x80 and a one byte
168eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // section type followed by a four byte section length.  Type one is
178eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // an ASCII section (includes a length), type two is a binary section
188eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // (includes a length) and type three is an EOF marker with no length.
198eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const uint8_t* buf = *src;
208eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (*len < 2 || buf[0] != 0x80 || buf[1] != sectionType) {
218eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
228eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    } else if (buf[1] == 3) {
238eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return true;
248eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    } else if (*len < 6) {
258eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
268eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
278eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
288eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *size = (size_t)buf[2] | ((size_t)buf[3] << 8) | ((size_t)buf[4] << 16) |
298eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            ((size_t)buf[5] << 24);
308eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    size_t consumed = *size + 6;
318eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (consumed > *len) {
328eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
338eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
348eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *src = *src + consumed;
358eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *len = *len - consumed;
368eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    return true;
378eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary}
388eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
398eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanarystatic bool parsePFB(const uint8_t* src, size_t size, size_t* headerLen,
408eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary                     size_t* dataLen, size_t* trailerLen) {
418eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const uint8_t* srcPtr = src;
428eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    size_t remaining = size;
438eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
448eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    return parsePFBSection(&srcPtr, &remaining, 1, headerLen) &&
458eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary           parsePFBSection(&srcPtr, &remaining, 2, dataLen) &&
468eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary           parsePFBSection(&srcPtr, &remaining, 1, trailerLen) &&
478eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary           parsePFBSection(&srcPtr, &remaining, 3, nullptr);
488eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary}
498eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
508eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary/* The sections of a PFA file are implicitly defined.  The body starts
518eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * after the line containing "eexec," and the trailer starts with 512
528eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * literal 0's followed by "cleartomark" (plus arbitrary white space).
538eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary *
548eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * This function assumes that src is NUL terminated, but the NUL
558eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary * termination is not included in size.
568eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary *
578eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary */
588eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanarystatic bool parsePFA(const char* src, size_t size, size_t* headerLen,
598eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary                     size_t* hexDataLen, size_t* dataLen, size_t* trailerLen) {
608eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const char* end = src + size;
618eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
628eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const char* dataPos = strstr(src, "eexec");
638eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!dataPos) {
648eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
658eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
668eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    dataPos += strlen("eexec");
678eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    while ((*dataPos == '\n' || *dataPos == '\r' || *dataPos == ' ') &&
688eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            dataPos < end) {
698eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        dataPos++;
708eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
718eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *headerLen = dataPos - src;
728eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
738eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const char* trailerPos = strstr(dataPos, "cleartomark");
748eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!trailerPos) {
758eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
768eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
778eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    int zeroCount = 0;
788eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    for (trailerPos--; trailerPos > dataPos && zeroCount < 512; trailerPos--) {
798eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        if (*trailerPos == '\n' || *trailerPos == '\r' || *trailerPos == ' ') {
808eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            continue;
818eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        } else if (*trailerPos == '0') {
828eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            zeroCount++;
838eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        } else {
848eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            return false;
858eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        }
868eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
878eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (zeroCount != 512) {
888eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return false;
898eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
908eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
918eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *hexDataLen = trailerPos - src - *headerLen;
928eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *trailerLen = size - *headerLen - *hexDataLen;
938eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
948eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // Verify that the data section is hex encoded and count the bytes.
958eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    int nibbles = 0;
968eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    for (; dataPos < trailerPos; dataPos++) {
978eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        if (isspace(*dataPos)) {
988eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            continue;
998eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        }
1008eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        if (!isxdigit(*dataPos)) {
1018eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            return false;
1028eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        }
1038eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        nibbles++;
1048eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1058eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    *dataLen = (nibbles + 1) / 2;
1068eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1078eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    return true;
1088eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary}
1098eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1108eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanarystatic int8_t hexToBin(uint8_t c) {
1118eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!isxdigit(c)) {
1128eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return -1;
1138eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    } else if (c <= '9') {
1148eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return c - '0';
1158eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    } else if (c <= 'F') {
1168eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return c - 'A' + 10;
1178eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    } else if (c <= 'f') {
1188eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return c - 'a' + 10;
1198eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1208eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    return -1;
1218eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary}
1228eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1238eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanarysk_sp<SkData> SkPDFConvertType1FontStream(
1248eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        std::unique_ptr<SkStreamAsset> srcStream, size_t* headerLen,
1258eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        size_t* dataLen, size_t* trailerLen) {
1268eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    size_t srcLen = srcStream ? srcStream->getLength() : 0;
1278eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    SkASSERT(srcLen);
1288eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!srcLen) {
1298eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return nullptr;
1308eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1318eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // Flatten and Nul-terminate the source stream so that we can use
1328eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // strstr() to search it.
1338eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    SkAutoTMalloc<uint8_t> sourceBuffer(SkToInt(srcLen + 1));
1348eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    (void)srcStream->read(sourceBuffer.get(), srcLen);
1358eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    sourceBuffer[SkToInt(srcLen)] = 0;
1368eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const uint8_t* src = sourceBuffer.get();
1378eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1388eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (parsePFB(src, srcLen, headerLen, dataLen, trailerLen)) {
1398eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        static const int kPFBSectionHeaderLength = 6;
1408eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        const size_t length = *headerLen + *dataLen + *trailerLen;
1418eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        SkASSERT(length > 0);
1428eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        SkASSERT(length + (2 * kPFBSectionHeaderLength) <= srcLen);
1438eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1448eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        sk_sp<SkData> data(SkData::MakeUninitialized(length));
1458eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1468eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        const uint8_t* const srcHeader = src + kPFBSectionHeaderLength;
1478eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        // There is a six-byte section header before header and data
1488eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        // (but not trailer) that we're not going to copy.
1498eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        const uint8_t* const srcData = srcHeader + *headerLen + kPFBSectionHeaderLength;
1508eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        const uint8_t* const srcTrailer = srcData + *headerLen;
1518eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1528eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        uint8_t* const resultHeader = (uint8_t*)data->writable_data();
1538eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        uint8_t* const resultData = resultHeader + *headerLen;
1548eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        uint8_t* const resultTrailer = resultData + *dataLen;
1558eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1568eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        SkASSERT(resultTrailer + *trailerLen == resultHeader + length);
1578eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1588eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        memcpy(resultHeader,  srcHeader,  *headerLen);
1598eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        memcpy(resultData,    srcData,    *dataLen);
1608eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        memcpy(resultTrailer, srcTrailer, *trailerLen);
1618eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1628eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return data;
1638eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1648eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1658eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    // A PFA has to be converted for PDF.
1668eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    size_t hexDataLen;
1678eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!parsePFA((const char*)src, srcLen, headerLen, &hexDataLen, dataLen,
1688eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary                 trailerLen)) {
1698eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        return nullptr;
1708eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1718eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const size_t length = *headerLen + *dataLen + *trailerLen;
1728eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    SkASSERT(length > 0);
1738eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    auto data = SkData::MakeUninitialized(length);
1748eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    uint8_t* buffer = (uint8_t*)data->writable_data();
1758eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1768eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    memcpy(buffer, src, *headerLen);
1778eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    uint8_t* const resultData = &(buffer[*headerLen]);
1788eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
1798eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const uint8_t* hexData = src + *headerLen;
1808eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    const uint8_t* trailer = hexData + hexDataLen;
1818eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    size_t outputOffset = 0;
1828eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    uint8_t dataByte = 0;  // To hush compiler.
1838eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    bool highNibble = true;
1848eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    for (; hexData < trailer; hexData++) {
1858eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        int8_t curNibble = hexToBin(*hexData);
1868eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        if (curNibble < 0) {
1878eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            continue;
1888eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        }
1898eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        if (highNibble) {
1908eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            dataByte = curNibble << 4;
1918eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            highNibble = false;
1928eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        } else {
1938eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            dataByte |= curNibble;
1948eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            highNibble = true;
1958eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary            resultData[outputOffset++] = dataByte;
1968eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        }
1978eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
1988eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    if (!highNibble) {
1998eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary        resultData[outputOffset++] = dataByte;
2008eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    }
2018eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    SkASSERT(outputOffset == *dataLen);
2028eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary
2038eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    uint8_t* const resultTrailer = &(buffer[SkToInt(*headerLen + outputOffset)]);
2048eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    memcpy(resultTrailer, src + *headerLen + hexDataLen, *trailerLen);
2058eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary    return data;
2068eccc308c8adcdf26ffc7c4dd538b71f33c6f22bhalcanary}
207