#!/usr/bin/env python
# Copyright 2012-2018 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2

doc = """Dump and restore extended attributes.

We use formats like that used by getfattr --dump.  This is meant for shell
helpers to save/restore.  If you're looking for a python/portage API, see
portage.util.movefile._copyxattr instead.

https://en.wikipedia.org/wiki/Extended_file_attributes
"""
__doc__ = doc


import argparse
import array
import os
import re
import sys

from portage.util._xattr import xattr


_UNQUOTE_RE = re.compile(rb"\\[0-7]{3}")
_FS_ENCODING = sys.getfilesystemencoding()


def octal_quote_byte(b):
    return ("\\%03o" % ord(b)).encode("ascii")


def unicode_encode(s):
    if isinstance(s, str):
        s = s.encode(_FS_ENCODING, "surrogateescape")
    return s


def quote(s, quote_chars):
    """Convert all |quote_chars| in |s| to escape sequences

    This is normally used to escape any embedded quotation marks.
    """
    quote_re = re.compile(b"[" + quote_chars + b"]")
    result = []
    pos = 0
    s_len = len(s)

    while pos < s_len:
        m = quote_re.search(s, pos=pos)
        if m is None:
            result.append(s[pos:])
            pos = s_len
        else:
            start = m.start()
            result.append(s[pos:start])
            result.append(octal_quote_byte(s[start : start + 1]))
            pos = start + 1

    return b"".join(result)


def unquote(s):
    """Process all escape sequences in |s|"""
    result = []
    pos = 0
    s_len = len(s)

    while pos < s_len:
        m = _UNQUOTE_RE.search(s, pos=pos)
        if m is None:
            result.append(s[pos:])
            pos = s_len
        else:
            start = m.start()
            result.append(s[pos:start])
            pos = start + 4
            a = array.array("B")
            a.append(int(s[start + 1 : pos], 8))
            try:
                # Python >= 3.2
                result.append(a.tobytes())
            except AttributeError:
                result.append(a.tostring())

    return b"".join(result)


def dump_xattrs(pathnames, file_out):
    """Dump the xattr data for |pathnames| to |file_out|"""
    # NOTE: Always quote backslashes, in order to ensure that they are
    # not interpreted as quotes when they are processed by unquote.
    quote_chars = b"\n\r\\\\"

    for pathname in pathnames:
        attrs = xattr.list(pathname)
        if not attrs:
            continue

        file_out.write(b"# file: %s\n" % quote(pathname, quote_chars))
        for attr in attrs:
            attr = unicode_encode(attr)
            value = xattr.get(pathname, attr)
            file_out.write(
                b'%s="%s"\n'
                % (quote(attr, b"=" + quote_chars), quote(value, b'\0"' + quote_chars))
            )


def restore_xattrs(file_in):
    """Read |file_in| and restore xattrs content from it

    This expects textual data in the format written by dump_xattrs.
    """
    pathname = None
    for i, line in enumerate(file_in):
        if line.startswith(b"# file: "):
            pathname = unquote(line.rstrip(b"\n")[8:])
        else:
            parts = line.split(b"=", 1)
            if len(parts) == 2:
                if pathname is None:
                    raise ValueError("line %d: missing pathname" % (i + 1,))
                attr = unquote(parts[0])
                # strip trailing newline and quotes
                value = unquote(parts[1].rstrip(b"\n")[1:-1])
                xattr.set(pathname, attr, value)
            elif line.strip():
                raise ValueError("line %d: malformed entry" % (i + 1,))


def main(argv):

    parser = argparse.ArgumentParser(description=doc)
    parser.add_argument("paths", nargs="*", default=[])

    actions = parser.add_argument_group("Actions")
    actions.add_argument(
        "--dump",
        action="store_true",
        help="Dump the values of all extended "
        "attributes associated with paths "
        "passed as arguments or null-separated "
        "paths read from stdin.",
    )
    actions.add_argument(
        "--restore",
        action="store_true",
        help="Restore extended attributes using " "a dump read from stdin.",
    )

    options = parser.parse_args(argv)

    file_in = sys.stdin.buffer.raw

    if options.dump:
        if options.paths:
            options.paths = [unicode_encode(x) for x in options.paths]
        else:
            options.paths = [x for x in file_in.read().split(b"\0") if x]
        file_out = sys.stdout.buffer
        dump_xattrs(options.paths, file_out)

    elif options.restore:
        restore_xattrs(file_in)

    else:
        parser.error("missing action!")

    return os.EX_OK


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
