view persistent-tags.sl @ 0:8eeb70d3d1ce

Initial revision
author Guido Berhoerster <guido+slrn@berhoerster.name>
date Sat, 14 Mar 2015 11:43:52 +0100
parents
children 49f639bc9bd9
line wrap: on
line source

% persistent-tags.sl - keep persistent tags across sessions
%
% Copyright (C) 2009 Guido Berhoerster <guido+slrn@berhoerster.name>
%
% Permission is hereby granted, free of charge, to any person obtaining
% a copy of this software and associated documentation files (the
% "Software"), to deal in the Software without restriction, including
% without limitation the rights to use, copy, modify, merge, publish,
% distribute, sublicense, and/or sell copies of the Software, and to
% permit persons to whom the Software is furnished to do so, subject to
% the following conditions:
%
% The above copyright notice and this permission notice shall be included
% in all copies or substantial portions of the Software.
%
% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

%open_log_file(make_home_filename("slrn-debug.log"));
%_traceback = 1;

implements("PersistentTags");

private variable rand_next = 1;

% implementation of rand based on an example in IEEE Std 1003.1, 2004 Edition
static define myrand() {
    rand_next = rand_next * 1103515245 + 12345;
    % RAND_MAX is hardcoded to 32767
    return ((rand_next / 65536U) mod 32768U);
}

static define mysrand(seed) {
    rand_next = seed;
}

private variable URL_SAFE_CHARS =
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+";

private define urldecode(str)
{
    variable decoded_str = ""B;
    variable char;
    variable pos = 1;
    variable opos = 1;

    forever {
        pos = string_match(str, "%[0-9a-fA-F][0-9a-fA-F]", opos);
        if (pos == 0)
            break;

        % add characters between the last match and the current match
        decoded_str += substr(str, opos, pos - opos);
        % convert the hex representation of a byte to a byte and append it to
        % the string
        char = integer("0x" + substr(str, pos + 1, 2));
        decoded_str += pack("C", char);
        opos = pos + 3;
    }
    % add remaining charcters
    decoded_str += substr(str, opos, -1);
    return typecast(decoded_str, String_Type);
}

private define urlencode(str)
{
    variable char;
    variable encoded_str = "";
    variable i;
    variable j;

    for (i = 0; i < strlen(str); ++i) {
        char = substr(str, i + 1, 1);
        ifnot (is_substr(URL_SAFE_CHARS, char)) {
            for (j = 0; j < strbytelen(char); ++j) {
                encoded_str += sprintf("%%%02X", char[j]);
            }
        } else {
            encoded_str += sprintf("%s", char);
        }
    }
    return encoded_str;
}

private variable FILENAME_CHARS =
        "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private variable FILENAME_CHARS_LEN = strlen(FILENAME_CHARS);

private define mkstemp(template)
{
    variable fd;
    variable tmp_filename;
    variable len_template = strlen(@template);
    variable c;

    if (len_template < 6)
        return NULL;
    c = len_template - 6;
    ifnot (substr(@template, c + 1, len_template) == "XXXXXX")
        return NULL;

    loop (10000) {
        tmp_filename = substr(@template, 1, c);
        loop(6) {
            tmp_filename += FILENAME_CHARS[[myrand() mod FILENAME_CHARS_LEN]];
        }
        fd = open(tmp_filename, O_CREAT|O_EXCL|O_RDWR, 0600);
        ifnot (fd == NULL) {
            @template = tmp_filename;
            break;
        }
    }

    return fd;
}

private define lock_file(filename)
{
    variable fd;
    variable st;
    variable timeout = qualifier("timeout", 30);
    variable stale_timeout = qualifier("stale_timeout", 360);
    variable lockfile = filename + ".lock";
    variable tmp_lockfile = lockfile + ".XXXXXX";
    variable time_timeout = _time() + timeout;
    variable time_stale_timeout = _time() - stale_timeout;

    fd = mkstemp(&tmp_lockfile);
    if (fd == NULL)
        return NULL;
    () = close(fd);
    try {
        % attempt to acquire a lock until time_timeout is reached
        while (_time() < time_timeout) {
            % try to link lockfile to the previously created temporary file,
            % link(2) is atomic even on NFSv2 if the lockfile exists link(2)
            % will fail, this is either detected if EEXIST is returned or the
            % link count of the temporary file is not 2 in this case try to
            % remove a stale lockfile, then wait and try again
            ifnot ((hardlink(tmp_lockfile, lockfile) == 0) ||
                    (errno == EEXIST))
                return NULL;

            st = stat_file(tmp_lockfile);
            if (st == NULL)
                return NULL;
            if (st.st_nlink == 2)
                return lockfile;

            st = stat_file(lockfile);
            if (st == NULL) {
                ifnot (errno == ENOENT)
                    return NULL;
                else
                    continue;
            }

            % remove a stale lockfile after stale_timeout seconds have passed
            if (st.st_mtime < time_stale_timeout)
                () = remove(lockfile);

            sleep(2);
        }

        return NULL;
    } finally {
        () = remove(tmp_lockfile);
    }
}

static variable config = struct
{
    tag_path = ".slrn-tags",
    autosave = 1
};

private variable tag_list = Assoc_Type[Null_Type];

private define update_tag_list(ref_tag_list)
{
    variable new_tag_list = Assoc_Type[Null_Type];
    variable needs_update = 0;
    variable msgid = NULL;

    call("header_bob");
    % the very first article must be treated specially so it will not be missed
    % when doing next_tagged_header()
    while (((msgid == NULL) && (get_header_flags() & HEADER_TAGGED)) ||
            next_tagged_header()) {
        msgid = extract_article_header("Message-ID");
        if (strlen(msgid) == 0)
            continue;
        new_tag_list[msgid] = NULL;

        % check if a new element which will go into new_tag_list also exists
        % in tag_list in order to find any difference between both lists
        ifnot (needs_update || assoc_key_exists(@ref_tag_list, msgid))
            needs_update = 1;
    }

    % if all elements of new_tag_list are also contained in tag_lists
    % check whether all elements of tag_lists are also contained in
    % new_tag_lists in order to find any difference between both lists
    ifnot (needs_update) {
        foreach msgid(@ref_tag_list) using("keys") {
            ifnot (assoc_key_exists(new_tag_list, msgid)) {
                needs_update = 1;
                break;
            }
        }
    }

    % replace tag_list with new_tag_list
    @ref_tag_list = new_tag_list;
    return needs_update;
}

static define save_tags()
{
    variable line, fp;
    variable dir = path_concat(make_home_filename(config.tag_path),
            urlencode(server_name()));
    variable filename = path_concat(dir, urlencode(current_newsgroup()));
    variable lockfile;

    ifnot (update_tag_list(&tag_list))
        return;

    if (mkdir(dir) == -1) {
        ifnot (errno == EEXIST)
            throw IOError, "mkdir $dir failed: "$ + errno_string(errno);
    }

    lockfile = lock_file(filename);
    if (lockfile == NULL)
        throw IOError, "failed to lock $filename"$;

    try {
        % if tag_list is empty remove the file
        if (length(tag_list) == 0) {
            () = remove(filename);
            return;
        }

        fp = fopen(filename, "w");
        if (fp == NULL)
            throw OpenError, "opening $filename failed: "$ +
                    errno_string(errno);
        foreach line(tag_list) using("keys") {
            if (fputs(line + "\n", fp) == -1)
                throw WriteError, "writing to $filename failed: "$ +
                        errno_string(errno);
        }
        () = fclose(fp);
    } finally {
        () = remove(lockfile);
    }
    return;
}

static define load_tags()
{
    variable fp, buf, msgid, pos;
    variable dir = path_concat(make_home_filename(config.tag_path),
            urlencode(server_name()));
    variable filename = path_concat(dir, urlencode(current_newsgroup()));

    % re-ininitalize tag_list
    tag_list = Assoc_Type[Null_Type];

    fp = fopen(filename, "r");
    if (fp == NULL) {
        if (errno == ENOENT)
            return;
        throw OpenError, "opening $filename failed: "$ + errno_string(errno);
    }

    % save position to restore after applying tags
    pos = extract_article_header("Message-ID");
    while (fgets(&buf, fp) != -1) {
        msgid = strtrim(buf);
        if (strlen(msgid)) {
            tag_list[msgid] = NULL;
            if (locate_header_by_msgid(msgid, 1))
                set_header_flags(get_header_flags() | HEADER_TAGGED);
        }
    }
    () = fclose(fp);
    () = locate_header_by_msgid(pos, 0);

    return;
}

mysrand(_time() * getpid());

if (config.autosave)
{
    () = register_hook("article_mode_hook", "PersistentTags->load_tags");
    () = register_hook("article_mode_quit_hook", "PersistentTags->save_tags");
}