Enable BytesWarnings.
[portage.git] / bin / xattr-helper.py
1 #!/usr/bin/python -bb
2 # Copyright 2012-2014 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 """Dump and restore extended attributes.
6
7 We use formats like that used by getfattr --dump.  This is meant for shell
8 helpers to save/restore.  If you're looking for a python/portage API, see
9 portage.util.movefile._copyxattr instead.
10
11 https://en.wikipedia.org/wiki/Extended_file_attributes
12 """
13
14 import array
15 import os
16 import re
17 import sys
18
19 from portage.util._argparse import ArgumentParser
20
21 if hasattr(os, "getxattr"):
22
23         class xattr(object):
24                 get = os.getxattr
25                 set = os.setxattr
26                 list = os.listxattr
27
28 else:
29         import xattr
30
31
32 _UNQUOTE_RE = re.compile(br'\\[0-7]{3}')
33 _FS_ENCODING = sys.getfilesystemencoding()
34
35
36 if sys.hexversion < 0x3000000:
37
38         def octal_quote_byte(b):
39                 return b'\\%03o' % ord(b)
40
41         def unicode_encode(s):
42                 if isinstance(s, unicode):
43                         s = s.encode(_FS_ENCODING)
44                 return s
45 else:
46
47         def octal_quote_byte(b):
48                 return ('\\%03o' % ord(b)).encode('ascii')
49
50         def unicode_encode(s):
51                 if isinstance(s, str):
52                         s = s.encode(_FS_ENCODING)
53                 return s
54
55
56 def quote(s, quote_chars):
57         """Convert all |quote_chars| in |s| to escape sequences
58
59         This is normally used to escape any embedded quotation marks.
60         """
61         quote_re = re.compile(b'[' + quote_chars + b']')
62         result = []
63         pos = 0
64         s_len = len(s)
65
66         while pos < s_len:
67                 m = quote_re.search(s, pos=pos)
68                 if m is None:
69                         result.append(s[pos:])
70                         pos = s_len
71                 else:
72                         start = m.start()
73                         result.append(s[pos:start])
74                         result.append(octal_quote_byte(s[start:start+1]))
75                         pos = start + 1
76
77         return b''.join(result)
78
79
80 def unquote(s):
81         """Process all escape sequences in |s|"""
82         result = []
83         pos = 0
84         s_len = len(s)
85
86         while pos < s_len:
87                 m = _UNQUOTE_RE.search(s, pos=pos)
88                 if m is None:
89                         result.append(s[pos:])
90                         pos = s_len
91                 else:
92                         start = m.start()
93                         result.append(s[pos:start])
94                         pos = start + 4
95                         a = array.array('B')
96                         a.append(int(s[start + 1:pos], 8))
97                         try:
98                                 # Python >= 3.2
99                                 result.append(a.tobytes())
100                         except AttributeError:
101                                 result.append(a.tostring())
102
103         return b''.join(result)
104
105
106 def dump_xattrs(pathnames, file_out):
107         """Dump the xattr data for |pathnames| to |file_out|"""
108         # NOTE: Always quote backslashes, in order to ensure that they are
109         # not interpreted as quotes when they are processed by unquote.
110         quote_chars = b'\n\r\\\\'
111
112         for pathname in pathnames:
113                 attrs = xattr.list(pathname)
114                 if not attrs:
115                         continue
116
117                 file_out.write(b'# file: %s\n' % quote(pathname, quote_chars))
118                 for attr in attrs:
119                         attr = unicode_encode(attr)
120                         value = xattr.get(pathname, attr)
121                         file_out.write(b'%s="%s"\n' % (
122                                 quote(attr, b'=' + quote_chars),
123                                 quote(value, b'\0"' + quote_chars)))
124
125
126 def restore_xattrs(file_in):
127         """Read |file_in| and restore xattrs content from it
128
129         This expects textual data in the format written by dump_xattrs.
130         """
131         pathname = None
132         for i, line in enumerate(file_in):
133                 if line.startswith(b'# file: '):
134                         pathname = unquote(line.rstrip(b'\n')[8:])
135                 else:
136                         parts = line.split(b'=', 1)
137                         if len(parts) == 2:
138                                 if pathname is None:
139                                         raise ValueError('line %d: missing pathname' % (i + 1,))
140                                 attr = unquote(parts[0])
141                                 # strip trailing newline and quotes
142                                 value = unquote(parts[1].rstrip(b'\n')[1:-1])
143                                 xattr.set(pathname, attr, value)
144                         elif line.strip():
145                                 raise ValueError('line %d: malformed entry' % (i + 1,))
146
147
148 def main(argv):
149
150         parser = ArgumentParser(description=__doc__)
151         parser.add_argument('paths', nargs='*', default=[])
152
153         actions = parser.add_argument_group('Actions')
154         actions.add_argument('--dump',
155                 action='store_true',
156                 help='Dump the values of all extended '
157                         'attributes associated with null-separated'
158                         ' paths read from stdin.')
159         actions.add_argument('--restore',
160                 action='store_true',
161                 help='Restore extended attributes using'
162                         ' a dump read from stdin.')
163
164         options = parser.parse_args(argv)
165
166         if sys.hexversion >= 0x3000000:
167                 file_in = sys.stdin.buffer.raw
168         else:
169                 file_in = sys.stdin
170         if not options.paths:
171                 options.paths += [x for x in file_in.read().split(b'\0') if x]
172
173         if options.dump:
174                 if sys.hexversion >= 0x3000000:
175                         file_out = sys.stdout.buffer
176                 else:
177                         file_out = sys.stdout
178                 dump_xattrs(options.paths, file_out)
179
180         elif options.restore:
181                 restore_xattrs(file_in)
182
183         else:
184                 parser.error('missing action!')
185
186         return os.EX_OK
187
188
189 if __name__ == '__main__':
190         sys.exit(main(sys.argv[1:]))