blob: 49c9815803689c1f328c6e6f9321492ab3ac2aa0 [file] [log] [blame]
#!/usr/bin/python -b
# 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(br'\\[0-7]{3}')
_FS_ENCODING = sys.getfilesystemencoding()
if sys.hexversion < 0x3000000:
def octal_quote_byte(b):
return b'\\%03o' % ord(b)
def unicode_encode(s):
if isinstance(s, unicode):
s = s.encode(_FS_ENCODING)
return s
else:
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)
if sys.hexversion >= 0x3000000:
file_in = sys.stdin.buffer.raw
else:
file_in = sys.stdin
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]
if sys.hexversion >= 0x3000000:
file_out = sys.stdout.buffer
else:
file_out = sys.stdout
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:]))