Gentoo Archives: gentoo-commits

From: Zac Medico <zmedico@g.o>
To: gentoo-commits@l.g.o
Subject: [gentoo-commits] proj/portage:master commit in: lib/portage/dbapi/, lib/portage/util/
Date: Sun, 11 Aug 2019 19:13:00
Message-Id: 1565550707.0b7eda500a0dcb98a67f33bf9ef25b202b358986.zmedico@gentoo
1 commit: 0b7eda500a0dcb98a67f33bf9ef25b202b358986
2 Author: Arfrever Frehtes Taifersar Arahesis <Arfrever <AT> Apache <DOT> Org>
3 AuthorDate: Wed Aug 7 17:06:11 2019 +0000
4 Commit: Zac Medico <zmedico <AT> gentoo <DOT> org>
5 CommitDate: Sun Aug 11 19:11:47 2019 +0000
6 URL: https://gitweb.gentoo.org/proj/portage.git/commit/?id=0b7eda50
7
8 dblink._collision_protect: Detect internal collisions.
9
10 Implement detection of internal collisions (between files of the same package,
11 located in separate directories in the installation image (${D}) corresponding
12 to merged directories in the target filesystem (${ROOT})).
13
14 This provides protection against overwriting some files when performing merging
15 of files from ${D} to ${ROOT} in some filesystem layouts (such as /-merged layout
16 or /usr-merged layout).
17
18 Internal collisions between identical files are silently ignored.
19
20 Bug: https://bugs.gentoo.org/690484
21 Signed-off-by: Arfrever Frehtes Taifersar Arahesis <Arfrever <AT> Apache.Org>
22 Signed-off-by: Zac Medico <zmedico <AT> gentoo.org>
23
24 lib/portage/dbapi/vartree.py | 55 ++++++++++++++++++--
25 lib/portage/util/_compare_files.py | 103 +++++++++++++++++++++++++++++++++++++
26 2 files changed, 154 insertions(+), 4 deletions(-)
27
28 diff --git a/lib/portage/dbapi/vartree.py b/lib/portage/dbapi/vartree.py
29 index e2fce7736..4f069474b 100644
30 --- a/lib/portage/dbapi/vartree.py
31 +++ b/lib/portage/dbapi/vartree.py
32 @@ -30,6 +30,7 @@ portage.proxy.lazyimport.lazyimport(globals(),
33 'portage.util:apply_secpass_permissions,ConfigProtect,ensure_dirs,' + \
34 'writemsg,writemsg_level,write_atomic,atomic_ofstream,writedict,' + \
35 'grabdict,normalize_path,new_protect_filename',
36 + 'portage.util._compare_files:compare_files',
37 'portage.util.digraph:digraph',
38 'portage.util.env_update:env_update',
39 'portage.util.install_mask:install_mask_dir,InstallMask',
40 @@ -87,6 +88,7 @@ import io
41 from itertools import chain
42 import logging
43 import os as _os
44 +import operator
45 import platform
46 import pwd
47 import re
48 @@ -3418,6 +3420,8 @@ class dblink(object):
49
50 os = _os_merge
51
52 + real_relative_paths = {}
53 +
54 collision_ignore = []
55 for x in portage.util.shlex_split(
56 self.settings.get("COLLISION_IGNORE", "")):
57 @@ -3469,8 +3473,13 @@ class dblink(object):
58 previous = current
59 progress_shown = True
60
61 - dest_path = normalize_path(
62 - os.path.join(destroot, f.lstrip(os.path.sep)))
63 + dest_path = normalize_path(os.path.join(destroot, f.lstrip(os.path.sep)))
64 +
65 + # Relative path with symbolic links resolved only in parent directories
66 + real_relative_path = os.path.join(os.path.realpath(os.path.dirname(dest_path)),
67 + os.path.basename(dest_path))[len(destroot):]
68 +
69 + real_relative_paths.setdefault(real_relative_path, []).append(f.lstrip(os.path.sep))
70
71 parent = os.path.dirname(dest_path)
72 if parent not in dirs:
73 @@ -3556,9 +3565,24 @@ class dblink(object):
74 break
75 if stopmerge:
76 collisions.append(f)
77 +
78 + internal_collisions = {}
79 + for real_relative_path, files in real_relative_paths.items():
80 + # Detect internal collisions between non-identical files.
81 + if len(files) >= 2:
82 + files.sort()
83 + for i in range(len(files) - 1):
84 + file1 = normalize_path(os.path.join(srcroot, files[i]))
85 + file2 = normalize_path(os.path.join(srcroot, files[i+1]))
86 + # Compare files, ignoring differences in times.
87 + differences = compare_files(file1, file2, skipped_types=("atime", "mtime", "ctime"))
88 + if differences:
89 + internal_collisions.setdefault(real_relative_path, {})[(files[i], files[i+1])] = differences
90 +
91 if progress_shown:
92 showMessage(_("100% done\n"))
93 - return collisions, dirs_ro, symlink_collisions, plib_collisions
94 +
95 + return collisions, internal_collisions, dirs_ro, symlink_collisions, plib_collisions
96
97 def _lstat_inode_map(self, path_iter):
98 """
99 @@ -4081,7 +4105,7 @@ class dblink(object):
100 if blocker.exists():
101 blockers.append(blocker)
102
103 - collisions, dirs_ro, symlink_collisions, plib_collisions = \
104 + collisions, internal_collisions, dirs_ro, symlink_collisions, plib_collisions = \
105 self._collision_protect(srcroot, destroot,
106 others_in_slot + blockers, filelist, linklist)
107
108 @@ -4109,6 +4133,29 @@ class dblink(object):
109 eerror(msg)
110 return 1
111
112 + if internal_collisions:
113 + msg = _("Package '%s' has internal collisions between non-identical files "
114 + "(located in separate directories in the installation image (${D}) "
115 + "corresponding to merged directories in the target "
116 + "filesystem (${ROOT})):") % self.settings.mycpv
117 + msg = textwrap.wrap(msg, 70)
118 + msg.append("")
119 + for k, v in sorted(internal_collisions.items(), key=operator.itemgetter(0)):
120 + msg.append("\t%s" % os.path.join(destroot, k.lstrip(os.path.sep)))
121 + for (file1, file2), differences in sorted(v.items()):
122 + msg.append("\t\t%s" % os.path.join(destroot, file1.lstrip(os.path.sep)))
123 + msg.append("\t\t%s" % os.path.join(destroot, file2.lstrip(os.path.sep)))
124 + msg.append("\t\t\tDifferences: %s" % ", ".join(differences))
125 + msg.append("")
126 + self._elog("eerror", "preinst", msg)
127 +
128 + msg = _("Package '%s' NOT merged due to internal collisions "
129 + "between non-identical files.") % self.settings.mycpv
130 + msg += _(" If necessary, refer to your elog messages for the whole "
131 + "content of the above message.")
132 + eerror(textwrap.wrap(msg, 70))
133 + return 1
134 +
135 if symlink_collisions:
136 # Symlink collisions need to be distinguished from other types
137 # of collisions, in order to avoid confusion (see bug #409359).
138
139 diff --git a/lib/portage/util/_compare_files.py b/lib/portage/util/_compare_files.py
140 new file mode 100644
141 index 000000000..bd993e501
142 --- /dev/null
143 +++ b/lib/portage/util/_compare_files.py
144 @@ -0,0 +1,103 @@
145 +# Copyright 2019 Gentoo Authors
146 +# Distributed under the terms of the GNU General Public License v2
147 +
148 +__all__ = ["compare_files"]
149 +
150 +import io
151 +import os
152 +import stat
153 +import sys
154 +
155 +from portage import _encodings
156 +from portage import _unicode_encode
157 +from portage.util._xattr import xattr
158 +
159 +def compare_files(file1, file2, skipped_types=()):
160 + """
161 + Compare metadata and contents of two files.
162 +
163 + @param file1: File 1
164 + @type file1: str
165 + @param file2: File 2
166 + @type file2: str
167 + @param skipped_types: Tuple of strings specifying types of properties excluded from comparison.
168 + Supported strings: type, mode, owner, group, device_number, xattr, atime, mtime, ctime, size, content
169 + @type skipped_types: tuple of str
170 + @rtype: tuple of str
171 + @return: Tuple of strings specifying types of properties different between compared files
172 + """
173 +
174 + file1_stat = os.lstat(_unicode_encode(file1, encoding=_encodings["fs"], errors="strict"))
175 + file2_stat = os.lstat(_unicode_encode(file2, encoding=_encodings["fs"], errors="strict"))
176 +
177 + differences = []
178 +
179 + if (file1_stat.st_dev, file1_stat.st_ino) == (file2_stat.st_dev, file2_stat.st_ino):
180 + return ()
181 +
182 + if "type" not in skipped_types and stat.S_IFMT(file1_stat.st_mode) != stat.S_IFMT(file2_stat.st_mode):
183 + differences.append("type")
184 + if "mode" not in skipped_types and stat.S_IMODE(file1_stat.st_mode) != stat.S_IMODE(file2_stat.st_mode):
185 + differences.append("mode")
186 + if "owner" not in skipped_types and file1_stat.st_uid != file2_stat.st_uid:
187 + differences.append("owner")
188 + if "group" not in skipped_types and file1_stat.st_gid != file2_stat.st_gid:
189 + differences.append("group")
190 + if "device_number" not in skipped_types and file1_stat.st_rdev != file2_stat.st_rdev:
191 + differences.append("device_number")
192 +
193 + if "xattr" not in skipped_types and sorted(xattr.get_all(file1, nofollow=True)) != sorted(xattr.get_all(file2, nofollow=True)):
194 + differences.append("xattr")
195 +
196 + if sys.hexversion >= 0x3030000:
197 + if "atime" not in skipped_types and file1_stat.st_atime_ns != file2_stat.st_atime_ns:
198 + differences.append("atime")
199 + if "mtime" not in skipped_types and file1_stat.st_mtime_ns != file2_stat.st_mtime_ns:
200 + differences.append("mtime")
201 + if "ctime" not in skipped_types and file1_stat.st_ctime_ns != file2_stat.st_ctime_ns:
202 + differences.append("ctime")
203 + else:
204 + if "atime" not in skipped_types and file1_stat.st_atime != file2_stat.st_atime:
205 + differences.append("atime")
206 + if "mtime" not in skipped_types and file1_stat.st_mtime != file2_stat.st_mtime:
207 + differences.append("mtime")
208 + if "ctime" not in skipped_types and file1_stat.st_ctime != file2_stat.st_ctime:
209 + differences.append("ctime")
210 +
211 + if "type" in differences:
212 + pass
213 + elif file1_stat.st_size != file2_stat.st_size:
214 + if "size" not in skipped_types:
215 + differences.append("size")
216 + if "content" not in skipped_types:
217 + differences.append("content")
218 + else:
219 + if "content" not in skipped_types:
220 + if stat.S_ISLNK(file1_stat.st_mode):
221 + file1_stream = io.BytesIO(os.readlink(_unicode_encode(file1,
222 + encoding=_encodings["fs"],
223 + errors="strict")))
224 + else:
225 + file1_stream = open(_unicode_encode(file1,
226 + encoding=_encodings["fs"],
227 + errors="strict"), "rb")
228 + if stat.S_ISLNK(file2_stat.st_mode):
229 + file2_stream = io.BytesIO(os.readlink(_unicode_encode(file2,
230 + encoding=_encodings["fs"],
231 + errors="strict")))
232 + else:
233 + file2_stream = open(_unicode_encode(file2,
234 + encoding=_encodings["fs"],
235 + errors="strict"), "rb")
236 + while True:
237 + file1_content = file1_stream.read(4096)
238 + file2_content = file2_stream.read(4096)
239 + if file1_content != file2_content:
240 + differences.append("content")
241 + break
242 + if not file1_content or not file2_content:
243 + break
244 + file1_stream.close()
245 + file2_stream.close()
246 +
247 + return tuple(differences)