1#!/usr/bin/env python
2
3##
4# Copyright (c) 2007 - 2010, 2013 Apple Inc.
5#
6# This is the MIT license.  This software may also be distributed under the
7# same terms as Python (the PSF license).
8#
9# Permission is hereby granted, free of charge, to any person obtaining a
10# copy of this software and associated documentation files (the "Software"),
11# to deal in the Software without restriction, including without limitation
12# the rights to use, copy, modify, merge, publish, distribute, sublicense,
13# and/or sell copies of the Software, and to permit persons to whom the
14# Software is furnished to do so, subject to the following conditions:
15#
16# The above copyright notice and this permission notice shall be included in
17# all copies or substantial portions of the Software.
18#
19# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
25# IN THE SOFTWARE.
26##
27
28import sys
29import os
30import getopt
31import xattr
32import binascii
33import string
34
35def usage(e=None):
36    if e:
37        print e
38        print ""
39
40    name = os.path.basename(sys.argv[0])
41    print "usage: %s [-l] [-r] [-s] [-v] [-x] file [file ...]" % (name,)
42    print "       %s -p [-l] [-r] [-s] [-v] [-x] attr_name file [file ...]" % (name,)
43    print "       %s -w [-r] [-s] [-x] attr_name attr_value file [file ...]" % (name,)
44    print "       %s -d [-r] [-s] attr_name file [file ...]" % (name,)
45    print "       %s -c [-r] [-s] file [file ...]" % (name,)
46    print ""
47    print "The first form lists the names of all xattrs on the given file(s)."
48    print "The second form (-p) prints the value of the xattr attr_name."
49    print "The third form (-w) sets the value of the xattr attr_name to the string attr_value."
50    print "The fourth form (-d) deletes the xattr attr_name."
51    print "The fifth form (-c) deletes (clears) all xattrs."
52    print ""
53    print "options:"
54    print "  -h: print this help"
55    print "  -l: print long format (attr_name: attr_value and hex output has offsets and"
56    print "      ascii representation)"
57    print "  -r: act recursively"
58    print "  -s: act on the symbolic link itself rather than what the link points to"
59    print "  -v: also print filename (automatic with -r and with multiple files)"
60    print "  -x: attr_value is represented as a hex string for input and output"
61
62    if e:
63        sys.exit(64)
64    else:
65        sys.exit(0)
66
67#ifdef PY3K
68_FILTER=b''.join(x >= 32 and x < 127 and chr(x).encode('utf-8') or b'.' for x in range(256))
69_WHITESPACE=b''.join(chr(x).encode() for x in range(33))
70#else
71_FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
72#endif
73
74def _dump(src, length=16, longfmt=0):
75    result=[]
76    for i in xrange(0, len(src), length):
77        s = src[i:i+length]
78#ifdef PY3K
79        hexa = ' '.join(["%02X"%x for x in s])
80#else
81        hexa = ' '.join(["%02X"%ord(x) for x in s])
82#endif
83        if longfmt:
84#ifdef PY3K
85            printable = s.translate(_FILTER).decode('utf-8')
86#else
87            printable = s.translate(_FILTER)
88#endif
89            result.append("%08X  %-*s |%s|" % (i, length*3, hexa, printable))
90        else:
91            result.append(hexa)
92    if longfmt:
93        result.append("%08x" % len(src))
94    return '\n'.join(result)
95
96status = 0
97
98def main():
99    global status
100    try:
101        (optargs, args) = getopt.getopt(sys.argv[1:], "cdhlprsvwx", ["help"])
102    except getopt.GetoptError, e:
103        usage(e)
104
105    attr_name   = None
106    attr_value  = None
107    clear       = False
108    delete      = False
109    dohex       = False
110    long_format = False
111    main_arg    = None
112    options     = 0     # 0 or XATTR_NOFOLLOW
113    read        = False
114    recursive   = False
115    verbose     = False
116    write       = False
117
118    for opt, arg in optargs:
119        if opt == "-c":
120            if main_arg and main_arg != opt:
121                usage("Can't specify %s with %s" % (main_arg, opt))
122            main_arg = opt
123            clear = True
124        elif opt == "-d":
125            if main_arg and main_arg != opt:
126                usage("Can't specify %s with %s" % (main_arg, opt))
127            main_arg = opt
128            delete = True
129        elif opt in ("-h", "--help"):
130            usage()
131        elif opt == "-l":
132            long_format = True
133        elif opt == "-p":
134            if main_arg and main_arg != opt:
135                usage("Can't specify %s with %s" % (main_arg, opt))
136            main_arg = opt
137            read = True
138        elif opt == "-r":
139            recursive = True
140        elif opt == "-s":
141            options = xattr.XATTR_NOFOLLOW
142        elif opt == "-v":
143            verbose = True
144        elif opt == "-w":
145            if main_arg and main_arg != opt:
146                usage("Can't specify %s with %s" % (main_arg, opt))
147            main_arg = opt
148            write = True
149        elif opt == "-x":
150            dohex = True
151
152    if write or delete:
153        if long_format:
154            usage("-l not allowed with -w or -p")
155
156    if read or write or delete:
157        if not args:
158            usage("No attr_name")
159#ifdef PY3K
160        # using os.fsencode() to retrieve the original bytes of the argument
161        attr_name = os.fsencode(args.pop(0))
162#else
163        attr_name = args.pop(0)
164#endif
165
166    if write:
167        if not args:
168            usage("No attr_value")
169#ifdef PY3K
170        # using os.fsencode() to retrieve the original bytes of the argument
171        attr_value = os.fsencode(args.pop(0))
172#else
173        attr_value = args.pop(0)
174#endif
175
176    if len(args) > 1:
177        multiple_files = True
178    else:
179        multiple_files = False
180
181    for filename in args:
182        def doSinglePathChange(filename,attr_name,attr_value,read,write,delete,recursive):
183            def onError(e):
184                global status
185                if not os.path.exists(filename):
186                    sys.stderr.write("xattr: No such file: %s\n" % (filename,))
187                else:
188                    sys.stderr.write("xattr: " + str(e) + "\n")
189                status = 1
190
191            def notPrintable(s):
192                try:
193                    s.decode('utf-8')
194#ifdef PY3K
195                    if s.find(b'\0') >= 0:
196#else
197                    if s.find('\0') >= 0:
198#endif
199                        return True
200                    return False
201                except UnicodeError:
202                    return True
203
204            if verbose or recursive or multiple_files:
205                file_prefix = "%s: " % filename
206            else:
207                file_prefix = ""
208
209            if recursive and os.path.isdir(filename) and not os.path.islink(filename):
210                listdir = os.listdir(filename)
211                for subfilename in listdir:
212                    doSinglePathChange(filename+'/'+subfilename,attr_name,attr_value,read,write,delete,recursive)
213
214            try:
215                attrs = xattr.xattr(filename, options)
216            except (IOError, OSError), e:
217                onError(e)
218                return
219
220            if write:
221                try:
222                    if dohex:
223                        # strip whitespace and unhexlify
224#ifdef PY3K
225                        attr_value = binascii.unhexlify(attr_value.translate(None, _WHITESPACE))
226#else
227                        attr_value = binascii.unhexlify(attr_value.translate(string.maketrans('', ''), string.whitespace))
228#endif
229                    attrs[attr_name] = attr_value
230                except (IOError, OSError, TypeError), e:
231                    onError(e)
232                    return
233
234            elif delete:
235                try:
236                    del attrs[attr_name]
237                except (IOError, OSError), e:
238                    onError(e)
239                    return
240                except KeyError:
241                    if not recursive:
242#ifdef PY3K
243                        onError("%s: No such xattr: %s" % (filename, attr_name.decode('utf-8'),))
244#else
245                        onError("%s: No such xattr: %s" % (filename, attr_name,))
246#endif
247                        return
248
249            elif clear:
250                try:
251                    attrs.clear()
252                except (IOError, OSError), e:
253                    onError(e)
254                    return
255
256            else:
257                try:
258                    if read:
259                        attr_names = (attr_name,)
260                    else:
261#ifdef PY3K
262                        attr_names = [a for a in attrs.keys()]
263#else
264                        attr_names = [a.encode('utf-8') for a in attrs.keys()]
265#endif
266                except (IOError, OSError), e:
267                    onError(e)
268                    return
269
270                for attr_name in attr_names:
271                    try:
272                        if long_format:
273                            if dohex or notPrintable(attrs[attr_name]):
274#ifdef PY3K
275                                print "%s%s:" % (file_prefix, attr_name.decode('utf-8'))
276#else
277                                print "%s%s:" % (file_prefix, attr_name)
278#endif
279                                print _dump(attrs[attr_name], longfmt=1)
280                            else:
281#ifdef PY3K
282                                print "%s%s: %s" % (file_prefix, attr_name.decode('utf-8'), attrs[attr_name].decode('utf-8'))
283#else
284                                print "%s%s: %s" % (file_prefix, attr_name, attrs[attr_name])
285#endif
286                        else:
287                            if read:
288                                if dohex or notPrintable(attrs[attr_name]):
289                                    if len(file_prefix) > 0:
290                                        print file_prefix
291                                    print _dump(attrs[attr_name])
292                                else:
293#ifdef PY3K
294                                    print "%s%s" % (file_prefix, attrs[attr_name].decode('utf-8'))
295#else
296                                    print "%s%s" % (file_prefix, attrs[attr_name])
297#endif
298                            else:
299#ifdef PY3K
300                                print "%s%s" % (file_prefix, attr_name.decode('utf-8'))
301#else
302                                print "%s%s" % (file_prefix, attr_name)
303#endif
304                    except (IOError, OSError), e:
305                        onError(e)
306                        return
307                    except KeyError:
308#ifdef PY3K
309                        onError("%s: No such xattr: %s" % (filename, attr_name.decode('utf-8')))
310#else
311                        onError("%s: No such xattr: %s" % (filename, attr_name))
312#endif
313                        return
314
315            return
316
317        doSinglePathChange(filename,attr_name,attr_value,read,write,delete,recursive)
318    sys.exit(status)
319
320if __name__ == "__main__":
321    main()
322