bool SaveFileModifictions(const WCHAR *filepath, Vec<PageAnnotation> *list)
{
    if (!list)
        return false;

    str::Str<char> data;
    data.AppendFmt("/* SumatraPDF: modifications to \"%S\" */\r\n", path::GetBaseName(filepath));
    data.AppendFmt("@meta { version: %s", SMX_CURR_VERSION);
    int64 size = file::GetSize(filepath);
    if (0 <= size && size <= UINT_MAX)
        data.AppendFmt("; filesize: %u", (UINT)size);
    data.Append(" }\r\n\r\n");

    for (size_t i = 0; i < list->Count(); i++) {
        PageAnnotation& annot = list->At(i);
        switch (annot.type) {
        case Annot_Highlight: data.Append("highlight"); break;
        case Annot_Underline: data.Append("underline"); break;
        case Annot_StrikeOut: data.Append("strikeout"); break;
        case Annot_Squiggly:  data.Append("squiggly "); break;
        default: continue;
        }
        data.AppendFmt(" { page: %d; rect: %.2f %.2f %.2f %.2f; color: #%02X%02X%02X%02X }\r\n",
                       annot.pageNo, annot.rect.x, annot.rect.y,
                       annot.rect.dx, annot.rect.dy, annot.color.a,
                       annot.color.r, annot.color.g, annot.color.b);
    }

    ScopedMem<WCHAR> modificationsPath(str::Join(filepath, SMX_FILE_EXT));
    return file::WriteAll(modificationsPath, data.LendData(), data.Size());
}
bool SaveFileModifictions(const WCHAR *filePath, Vec<PageAnnotation> *list)
{
    if (!list)
        return false;

    ScopedMem<WCHAR> modificationsPath(str::Join(filePath, SMX_FILE_EXT));
    str::Str<char> data;
    size_t offset = 0;

    ScopedMem<char> prevData(file::ReadAll(modificationsPath, NULL));
    Vec<PageAnnotation> *prevList = ParseFileModifications(prevData);
    if (prevList) {
        // in the case of an update, append changed annotations to the existing ones
        // (don't rewrite the existing ones in case they're by a newer version which
        // added annotation types and properties this version doesn't know anything about)
        for (; offset < prevList->Count() && prevList->At(offset) == list->At(offset); offset++);
        CrashIf(offset != prevList->Count());
        data.AppendAndFree(prevData.StealData());
        delete prevList;
    }
    else {
        data.AppendFmt("# SumatraPDF: modifications to \"%S\"\r\n", path::GetBaseName(filePath));
    }
    data.Append("\r\n");

    if (list->Count() == offset)
        return true; // nothing (new) to save

    data.AppendFmt("[@%s]\r\n", prevList ? "update" : "meta");
    data.AppendFmt("version = %s\r\n", SMX_CURR_VERSION);
    int64 size = file::GetSize(filePath);
    if (0 <= size && size <= UINT_MAX)
        data.AppendFmt("filesize = %u\r\n", (UINT)size);
    SYSTEMTIME time;
    GetSystemTime(&time);
    data.AppendFmt("timestamp = %04d-%02d-%02dT%02d:%02d:%02dZ\r\n",
        time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond);
    data.Append("\r\n");

    for (size_t i = offset; i < list->Count(); i++) {
        PageAnnotation& annot = list->At(i);
        switch (annot.type) {
        case Annot_Highlight: data.Append("[highlight]\r\n"); break;
        case Annot_Underline: data.Append("[underline]\r\n"); break;
        case Annot_StrikeOut: data.Append("[strikeout]\r\n"); break;
        case Annot_Squiggly:  data.Append("[squiggly]\r\n");  break;
        default: continue;
        }
        data.AppendFmt("page = %d\r\n", annot.pageNo);
        data.AppendFmt("rect = %g %g %g %g\r\n", annot.rect.x, annot.rect.y, annot.rect.dx, annot.rect.dy);
        data.AppendFmt("color = #%02x%02x%02x\r\n", annot.color.r, annot.color.g, annot.color.b);
        data.AppendFmt("opacity = %g\r\n", annot.color.a / 255.f);
        data.Append("\r\n");
    }
    data.RemoveAt(data.Size() - 2, 2);

    FileTransaction trans;
    return trans.WriteAll(modificationsPath, data.LendData(), data.Size()) && trans.Commit();
}
Vec<PageAnnotation> *LoadFileModifications(const WCHAR *filePath)
{
    ScopedMem<WCHAR> modificationsPath(str::Join(filePath, SMX_FILE_EXT));
    ScopedMem<char> data(file::ReadAll(modificationsPath, NULL));
    return ParseFileModifications(data);
}
Vec<PageAnnotation> *LoadFileModifications(const WCHAR *filepath)
{
    ScopedMem<WCHAR> modificationsPath(str::Join(filepath, SMX_FILE_EXT));
    size_t len;
    ScopedMem<char> data(file::ReadAll(modificationsPath, &len));
    if (!data)
        return NULL;

    CssPullParser parser(data, len);
    if (!parser.NextRule())
        return NULL;
    const CssSelector *sel = parser.NextSelector();
    if (!sel || !IsSelector(sel, "@meta") || parser.NextSelector())
        return NULL;
    const CssProperty *prop = parser.NextProperty();
    if (!prop || Css_Version != prop->type || !str::Parse(prop->s, prop->sLen, SMX_CURR_VERSION "%$"))
        return NULL;

    Vec<PageAnnotation> *list = new Vec<PageAnnotation>();
    while (parser.NextRule()) {
        sel = parser.NextSelector();
        if (!sel)
            continue;
        PageAnnotType type = IsSelector(sel, "highlight") ? Annot_Highlight :
                             IsSelector(sel, "underline") ? Annot_Underline :
                             IsSelector(sel, "strikeout") ? Annot_StrikeOut :
                             IsSelector(sel, "squiggly")  ? Annot_Squiggly  :
                             Annot_None;
        if (Annot_None == type || parser.NextSelector())
            continue;

        int pageNo = 0;
        RectT<float> rect;
        PageAnnotation::Color color;
        while ((prop = parser.NextProperty())) {
            switch (prop->type) {
            case Css_Page:
                if (!str::Parse(prop->s, prop->sLen, "%d%$", &pageNo))
                    pageNo = 0;
                break;
            case Css_Rect:
                if (!str::Parse(prop->s, prop->sLen, "%f %f %f %f%$",
                                &rect.x, &rect.y, &rect.dx, &rect.dy))
                    rect = RectT<float>();
                break;
            case Css_Color:
                int r, g, b, a;
                if (str::Parse(prop->s, prop->sLen, "#%2x%2x%2x%2x%$", &a, &r, &g, &b))
                    color = PageAnnotation::Color(r, g, b, a);
                else if (str::Parse(prop->s, prop->sLen, "#%2x%2x%2x%$", &r, &g, &b))
                    color = PageAnnotation::Color(r, g, b);
                break;
            }
        }
        if (pageNo <= 0 || rect.IsEmpty())
            continue;
        list->Append(PageAnnotation(type, pageNo, rect.Convert<double>(), color));
    }

    return list;
}