#!/usr/bin/python2.7 -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:]))
