c35acd1459c0187308603cb4e1d4681e1593897c
[portage.git] / bin / chpathtool.py
1 #!/usr/bin/python
2 # Copyright 2011-2013 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4
5 import io
6 import os
7 import stat
8 import sys
9
10 from portage.util._argparse import ArgumentParser
11
12 CONTENT_ENCODING = "utf_8"
13 FS_ENCODING = "utf_8"
14
15 try:
16         import magic
17 except ImportError:
18         magic = None
19 else:
20         try:
21                 magic.MIME_TYPE
22         except AttributeError:
23                 # magic module seems to be broken
24                 magic = None
25
26 class IsTextFile(object):
27
28         def __init__(self):
29                 if magic is not None:
30                         self._call = self._is_text_magic
31                         self._m = magic.open(magic.MIME_TYPE)
32                         self._m.load()
33                 else:
34                         self._call = self._is_text_encoding
35                         self._encoding = CONTENT_ENCODING
36
37         def __call__(self, filename):
38                 """
39                 Returns True if the given file is a text file, and False otherwise.
40                 """
41                 return self._call(filename)
42
43         def _is_text_magic(self, filename):
44                 mime_type = self._m.file(filename)
45                 if isinstance(mime_type, bytes):
46                         mime_type = mime_type.decode('ascii', 'replace')
47                 return mime_type.startswith("text/")
48
49         def _is_text_encoding(self, filename):
50                 try:
51                         for line in io.open(filename, mode='r', encoding=self._encoding):
52                                 pass
53                 except UnicodeDecodeError:
54                         return False
55                 return True
56
57 def chpath_inplace(filename, is_text_file, old, new):
58         """
59         Returns True if any modifications were made, and False otherwise.
60         """
61
62         modified = False
63         orig_stat = os.lstat(filename)
64         try:
65                 f = io.open(filename, buffering=0, mode='r+b')
66         except IOError:
67                 try:
68                         orig_mode = stat.S_IMODE(os.lstat(filename).st_mode)
69                 except OSError as e:
70                         sys.stderr.write("%s: %s\n" % (e, filename))
71                         return
72                 temp_mode = 0o200 | orig_mode
73                 os.chmod(filename, temp_mode)
74                 try:
75                         f = io.open(filename, buffering=0, mode='r+b')
76                 finally:
77                         os.chmod(filename, orig_mode)
78
79         len_old = len(old)
80         len_new = len(new)
81         matched_byte_count = 0
82         while True:
83                 in_byte = f.read(1)
84
85                 if not in_byte:
86                         break
87
88                 if in_byte == old[matched_byte_count]:
89                         matched_byte_count += 1
90                         if matched_byte_count == len_old:
91                                 modified = True
92                                 matched_byte_count = 0
93                                 end_position = f.tell()
94                                 start_position = end_position - len_old
95                                 if not is_text_file:
96                                         # search backwards for leading slashes written by
97                                         # a previous invocation of this tool
98                                         num_to_write = len_old
99                                         f.seek(start_position - 1)
100                                         while True:
101                                                 if f.read(1) != b'/':
102                                                         break
103                                                 num_to_write += 1
104                                                 f.seek(f.tell() - 2)
105
106                                         # pad with as many leading slashes as necessary
107                                         while num_to_write > len_new:
108                                                 f.write(b'/')
109                                                 num_to_write -= 1
110                                         f.write(new)
111                                 else:
112                                         remainder = f.read()
113                                         f.seek(start_position)
114                                         f.write(new)
115                                         if remainder:
116                                                 f.write(remainder)
117                                                 f.truncate()
118                                                 f.seek(start_position + len_new)
119                 elif matched_byte_count > 0:
120                         # back up an try to start a new match after
121                         # the first byte of the previous partial match
122                         f.seek(f.tell() - matched_byte_count)
123                         matched_byte_count = 0
124
125         f.close()
126         if modified:
127                 if sys.hexversion >= 0x3030000:
128                         orig_mtime = orig_stat.st_mtime_ns
129                         os.utime(filename, ns=(orig_mtime, orig_mtime))
130                 else:
131                         orig_mtime = orig_stat[stat.ST_MTIME]
132                         os.utime(filename, (orig_mtime, orig_mtime))
133         return modified
134
135 def chpath_inplace_symlink(filename, st, old, new):
136         target = os.readlink(filename)
137         if target.startswith(old):
138                 new_target = new + target[len(old):]
139                 os.unlink(filename)
140                 os.symlink(new_target, filename)
141                 os.lchown(filename, st.st_uid, st.st_gid)
142
143 def main(argv):
144
145         usage = "%s [options] <location> <old> <new>" % (os.path.basename(argv[0],))
146         parser = ArgumentParser(usage=usage)
147         options, args = parser.parse_known_args(argv[1:])
148
149         if len(args) != 3:
150                 parser.error("3 args required, got %s" % (len(args),))
151
152         location, old, new = args
153
154         is_text_file = IsTextFile()
155
156         if not isinstance(location, bytes):
157                 location = location.encode(FS_ENCODING)
158         if not isinstance(old, bytes):
159                 old = old.encode(FS_ENCODING)
160         if not isinstance(new, bytes):
161                 new = new.encode(FS_ENCODING)
162
163         st = os.lstat(location)
164
165         if stat.S_ISDIR(st.st_mode):
166                 for parent, dirs, files in os.walk(location):
167                         for filename in files:
168                                 filename = os.path.join(parent, filename)
169                                 try:
170                                         st = os.lstat(filename)
171                                 except OSError:
172                                         pass
173                                 else:
174                                         if stat.S_ISREG(st.st_mode):
175                                                 chpath_inplace(filename,
176                                                         is_text_file(filename), old, new)
177                                         elif stat.S_ISLNK(st.st_mode):
178                                                 chpath_inplace_symlink(filename, st, old, new)
179
180         elif stat.S_ISREG(st.st_mode):
181                 chpath_inplace(location,
182                         is_text_file(location), old, new)
183         elif stat.S_ISLNK(st.st_mode):
184                 chpath_inplace_symlink(location, st, old, new)
185
186         return os.EX_OK
187
188 if __name__ == "__main__":
189         sys.exit(main(sys.argv))