summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/binary_distribution.py8
-rw-r--r--lib/spack/spack/filesystem_view.py4
-rw-r--r--lib/spack/spack/relocate.py248
-rw-r--r--lib/spack/spack/relocate_text.py288
-rw-r--r--lib/spack/spack/rewiring.py4
-rw-r--r--lib/spack/spack/test/packaging.py6
-rw-r--r--lib/spack/spack/test/relocate.py240
-rw-r--r--lib/spack/spack/test/relocate_text.py247
8 files changed, 561 insertions, 484 deletions
diff --git a/lib/spack/spack/binary_distribution.py b/lib/spack/spack/binary_distribution.py
index 9cd36ec303..bc3b0089a7 100644
--- a/lib/spack/spack/binary_distribution.py
+++ b/lib/spack/spack/binary_distribution.py
@@ -47,7 +47,7 @@ import spack.util.spack_yaml as syaml
import spack.util.url as url_util
import spack.util.web as web_util
from spack.caches import misc_cache_location
-from spack.relocate import utf8_paths_to_single_binary_regex
+from spack.relocate_text import utf8_paths_to_single_binary_regex
from spack.spec import Spec
from spack.stage import Stage
from spack.util.executable import which
@@ -1730,16 +1730,16 @@ def relocate_package(spec, allow_root):
# For all buildcaches
# relocate the install prefixes in text files including dependencies
- relocate.unsafe_relocate_text(text_names, prefix_to_prefix_text)
+ relocate.relocate_text(text_names, prefix_to_prefix_text)
# relocate the install prefixes in binary files including dependencies
- relocate.unsafe_relocate_text_bin(files_to_relocate, prefix_to_prefix_bin)
+ relocate.relocate_text_bin(files_to_relocate, prefix_to_prefix_bin)
# If we are installing back to the same location
# relocate the sbang location if the spack directory changed
else:
if old_spack_prefix != new_spack_prefix:
- relocate.unsafe_relocate_text(text_names, prefix_to_prefix_text)
+ relocate.relocate_text(text_names, prefix_to_prefix_text)
def _extract_inner_tarball(spec, filename, extract_to, unsigned, remote_checksum):
diff --git a/lib/spack/spack/filesystem_view.py b/lib/spack/spack/filesystem_view.py
index 79237d1574..41ef8c93a8 100644
--- a/lib/spack/spack/filesystem_view.py
+++ b/lib/spack/spack/filesystem_view.py
@@ -90,11 +90,11 @@ def view_copy(src, dst, view, spec=None):
prefix_to_projection[dep.prefix] = view.get_projection_for_spec(dep)
if spack.relocate.is_binary(dst):
- spack.relocate.unsafe_relocate_text_bin(binaries=[dst], prefixes=prefix_to_projection)
+ spack.relocate.relocate_text_bin(binaries=[dst], prefixes=prefix_to_projection)
else:
prefix_to_projection[spack.store.layout.root] = view._root
prefix_to_projection[orig_sbang] = new_sbang
- spack.relocate.unsafe_relocate_text(files=[dst], prefixes=prefix_to_projection)
+ spack.relocate.relocate_text(files=[dst], prefixes=prefix_to_projection)
try:
stat = os.stat(src)
os.chown(dst, stat.st_uid, stat.st_gid)
diff --git a/lib/spack/spack/relocate.py b/lib/spack/spack/relocate.py
index 8789a5def5..fd0a2d8cc1 100644
--- a/lib/spack/spack/relocate.py
+++ b/lib/spack/spack/relocate.py
@@ -4,7 +4,6 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import collections
import itertools
-import multiprocessing.pool
import os
import re
import shutil
@@ -27,6 +26,8 @@ import spack.store
import spack.util.elf as elf
import spack.util.executable as executable
+from .relocate_text import BinaryFilePrefixReplacer, TextFilePrefixReplacer
+
is_macos = str(spack.platforms.real_host()) == "darwin"
@@ -46,49 +47,6 @@ class InstallRootStringError(spack.error.SpackError):
)
-class BinaryStringReplacementError(spack.error.SpackError):
- def __init__(self, file_path, old_len, new_len):
- """The size of the file changed after binary path substitution
-
- Args:
- file_path (str): file with changing size
- old_len (str): original length of the file
- new_len (str): length of the file after substitution
- """
- super(BinaryStringReplacementError, self).__init__(
- "Doing a binary string replacement in %s failed.\n"
- "The size of the file changed from %s to %s\n"
- "when it should have remanined the same." % (file_path, old_len, new_len)
- )
-
-
-class BinaryTextReplaceError(spack.error.SpackError):
- def __init__(self, msg):
- msg += (
- " To fix this, compile with more padding "
- "(config:install_tree:padded_length), or install to a shorter prefix."
- )
- super(BinaryTextReplaceError, self).__init__(msg)
-
-
-class CannotGrowString(BinaryTextReplaceError):
- def __init__(self, old, new):
- msg = "Cannot replace {!r} with {!r} because the new prefix is longer.".format(old, new)
- super(CannotGrowString, self).__init__(msg)
-
-
-class CannotShrinkCString(BinaryTextReplaceError):
- def __init__(self, old, new, full_old_string):
- # Just interpolate binary string to not risk issues with invalid
- # unicode, which would be really bad user experience: error in error.
- # We have no clue if we actually deal with a real C-string nor what
- # encoding it has.
- msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
- old, new, full_old_string
- )
- super(CannotShrinkCString, self).__init__(msg)
-
-
@memoized
def _patchelf():
"""Return the full path to the patchelf binary, if available, else None."""
@@ -450,108 +408,6 @@ def needs_text_relocation(m_type, m_subtype):
return m_type == "text"
-def apply_binary_replacements(f, prefix_to_prefix, suffix_safety_size=7):
- """
- Given a file opened in rb+ mode, apply the string replacements as
- specified by an ordered dictionary of prefix to prefix mappings. This
- method takes special care of null-terminated C-strings. C-string constants
- are problematic because compilers and linkers optimize readonly strings for
- space by aliasing those that share a common suffix (only suffix since all
- of them are null terminated). See https://github.com/spack/spack/pull/31739
- and https://github.com/spack/spack/pull/32253 for details. Our logic matches
- the original prefix with a ``suffix_safety_size + 1`` lookahead for null bytes.
- If no null terminator is found, we simply pad with leading /, assuming that
- it's a long C-string; the full C-string after replacement has a large suffix
- in common with its original value.
- If there *is* a null terminator we can do the same as long as the replacement
- has a sufficiently long common suffix with the original prefix.
- As a last resort when the replacement does not have a long enough common suffix,
- we can try to shorten the string, but this only works if the new length is
- sufficiently short (typically the case when going from large padding -> normal path)
- If the replacement string is longer, or all of the above fails, we error out.
-
- Arguments:
- f: file opened in rb+ mode
- prefix_to_prefix (OrderedDict): OrderedDictionary where the keys are
- bytes representing the old prefixes and the values are the new
- suffix_safety_size (int): in case of null terminated strings, what size
- of the suffix should remain to avoid aliasing issues?
- """
- assert suffix_safety_size >= 0
- assert f.tell() == 0
-
- # Look for exact matches of our paths, and also look if there's a null terminator
- # soon after (this covers the case where we search for /abc but match /abc/ with
- # a trailing dir seperator).
- regex = re.compile(
- b"("
- + b"|".join(re.escape(p) for p in prefix_to_prefix.keys())
- + b")([^\0]{0,%d}\0)?" % suffix_safety_size
- )
-
- # We *could* read binary data in chunks to avoid loading all in memory,
- # but it's nasty to deal with matches across boundaries, so let's stick to
- # something simple.
-
- for match in regex.finditer(f.read()):
- # The matching prefix (old) and its replacement (new)
- old = match.group(1)
- new = prefix_to_prefix[old]
-
- # Did we find a trailing null within a N + 1 bytes window after the prefix?
- null_terminated = match.end(0) > match.end(1)
-
- # Suffix string length, excluding the null byte
- # Only makes sense if null_terminated
- suffix_strlen = match.end(0) - match.end(1) - 1
-
- # How many bytes are we shrinking our string?
- bytes_shorter = len(old) - len(new)
-
- # We can't make strings larger.
- if bytes_shorter < 0:
- raise CannotGrowString(old, new)
-
- # If we don't know whether this is a null terminated C-string (we're looking
- # only N + 1 bytes ahead), or if it is and we have a common suffix, we can
- # simply pad with leading dir separators.
- elif (
- not null_terminated
- or suffix_strlen >= suffix_safety_size # == is enough, but let's be defensive
- or old[-suffix_safety_size + suffix_strlen :]
- == new[-suffix_safety_size + suffix_strlen :]
- ):
- replacement = b"/" * bytes_shorter + new
-
- # If it *was* null terminated, all that matters is that we can leave N bytes
- # of old suffix in place. Note that > is required since we also insert an
- # additional null terminator.
- elif bytes_shorter > suffix_safety_size:
- replacement = new + match.group(2) # includes the trailing null
-
- # Otherwise... we can't :(
- else:
- raise CannotShrinkCString(old, new, match.group()[:-1])
-
- f.seek(match.start())
- f.write(replacement)
-
-
-def _replace_prefix_bin(filename, prefix_to_prefix):
- """Replace all the occurrences of the old prefix with a new prefix in binary
- files. See :func:`~spack.relocate.apply_binary_replacements` for details.
-
- Args:
- filename (str): target binary file
- byte_prefixes (OrderedDict): ordered dictionary where the keys are
- bytes representing the old prefixes and the values are the new
- prefixes (all bytes utf-8 encoded)
- """
-
- with open(filename, "rb+") as f:
- apply_binary_replacements(f, prefix_to_prefix)
-
-
def relocate_macho_binaries(
path_names,
old_layout_root,
@@ -800,120 +656,32 @@ def relocate_links(links, prefix_to_prefix):
symlink(new_target, link)
-def utf8_path_to_binary_regex(prefix):
- """Create a (binary) regex that matches the input path in utf8"""
- prefix_bytes = re.escape(prefix).encode("utf-8")
- return re.compile(b"(?<![\\w\\-_/])([\\w\\-_]*?)%s([\\w\\-_/]*)" % prefix_bytes)
-
-
-def byte_strings_to_single_binary_regex(prefixes):
- all_prefixes = b"|".join(re.escape(p) for p in prefixes)
- return re.compile(b"(?<![\\w\\-_/])([\\w\\-_]*?)(%s)([\\w\\-_/]*)" % all_prefixes)
-
-
-def utf8_paths_to_single_binary_regex(prefixes):
- """Create a (binary) regex that matches any input path in utf8"""
- return byte_strings_to_single_binary_regex(p.encode("utf-8") for p in prefixes)
-
-
-def _replace_prefix_text_file(file, regex, prefix_to_prefix):
- """Given a text file opened in rb+, substitute all old with new prefixes and write
- in-place (file size may grow or shrink)."""
-
- def replacement(match):
- return match.group(1) + prefix_to_prefix[match.group(2)] + match.group(3)
-
- data = file.read()
- file.seek(0)
- file.write(re.sub(regex, replacement, data))
- file.truncate()
-
-
-def _replace_prefix_text(filename, regex, prefix_to_prefix):
- with open(filename, "rb+") as f:
- _replace_prefix_text_file(f, regex, prefix_to_prefix)
-
-
-def unsafe_relocate_text(files, prefixes, concurrency=32):
+def relocate_text(files, prefixes):
"""Relocate text file from the original installation prefix to the
new prefix.
Relocation also affects the the path in Spack's sbang script.
- Note: unsafe when files contains duplicates, such as repeated paths,
- symlinks, hardlinks.
-
Args:
files (list): Text files to be relocated
prefixes (OrderedDict): String prefixes which need to be changed
- concurrency (int): Preferred degree of parallelism
"""
+ TextFilePrefixReplacer.from_strings_or_bytes(prefixes).apply(files)
- # This now needs to be handled by the caller in all cases
- # orig_sbang = '#!/bin/bash {0}/bin/sbang'.format(orig_spack)
- # new_sbang = '#!/bin/bash {0}/bin/sbang'.format(new_spack)
-
- # Transform to binary string
- prefix_to_prefix = OrderedDict(
- (k.encode("utf-8"), v.encode("utf-8")) for (k, v) in prefixes.items()
- )
- # Create a regex of the form (pre check)(prefix 1|prefix 2|prefix 3)(post check).
- regex = byte_strings_to_single_binary_regex(prefix_to_prefix.keys())
-
- args = [(filename, regex, prefix_to_prefix) for filename in files]
- tp = multiprocessing.pool.ThreadPool(processes=concurrency)
- try:
- tp.map(llnl.util.lang.star(_replace_prefix_text), args)
- finally:
- tp.terminate()
- tp.join()
-
-
-def unsafe_relocate_text_bin(binaries, prefixes, concurrency=32):
- """Replace null terminated path strings hard coded into binaries.
+def relocate_text_bin(binaries, prefixes):
+ """Replace null terminated path strings hard-coded into binaries.
The new install prefix must be shorter than the original one.
- Note: unsafe when files contains duplicates, such as repeated paths,
- symlinks, hardlinks.
-
Args:
binaries (list): binaries to be relocated
prefixes (OrderedDict): String prefixes which need to be changed.
- concurrency (int): Desired degree of parallelism.
Raises:
- BinaryTextReplaceError: when the new path is longer than the old path
+ spack.relocate_text.BinaryTextReplaceError: when the new path is longer than the old path
"""
- byte_prefixes = collections.OrderedDict({})
-
- for orig_prefix, new_prefix in prefixes.items():
- if orig_prefix != new_prefix:
- if isinstance(orig_prefix, bytes):
- orig_bytes = orig_prefix
- else:
- orig_bytes = orig_prefix.encode("utf-8")
- if isinstance(new_prefix, bytes):
- new_bytes = new_prefix
- else:
- new_bytes = new_prefix.encode("utf-8")
- byte_prefixes[orig_bytes] = new_bytes
-
- # Do relocations on text in binaries that refers to the install tree
- # multiprocesing.ThreadPool.map requires single argument
- args = []
-
- for binary in binaries:
- args.append((binary, byte_prefixes))
-
- tp = multiprocessing.pool.ThreadPool(processes=concurrency)
-
- try:
- tp.map(llnl.util.lang.star(_replace_prefix_bin), args)
- finally:
- tp.terminate()
- tp.join()
+ BinaryFilePrefixReplacer.from_strings_or_bytes(prefixes).apply(binaries)
def is_relocatable(spec):
diff --git a/lib/spack/spack/relocate_text.py b/lib/spack/spack/relocate_text.py
new file mode 100644
index 0000000000..5d91f75474
--- /dev/null
+++ b/lib/spack/spack/relocate_text.py
@@ -0,0 +1,288 @@
+# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+
+"""This module contains pure-Python classes and functions for replacing
+paths inside text files and binaries."""
+
+import re
+from collections import OrderedDict
+from typing import Dict, Union
+
+import spack.error
+
+Prefix = Union[str, bytes]
+
+
+def encode_path(p: Prefix) -> bytes:
+ return p if isinstance(p, bytes) else p.encode("utf-8")
+
+
+def _prefix_to_prefix_as_bytes(prefix_to_prefix) -> Dict[bytes, bytes]:
+ return OrderedDict((encode_path(k), encode_path(v)) for (k, v) in prefix_to_prefix.items())
+
+
+def utf8_path_to_binary_regex(prefix: str):
+ """Create a binary regex that matches the input path in utf8"""
+ prefix_bytes = re.escape(prefix).encode("utf-8")
+ return re.compile(b"(?<![\\w\\-_/])([\\w\\-_]*?)%s([\\w\\-_/]*)" % prefix_bytes)
+
+
+def _byte_strings_to_single_binary_regex(prefixes):
+ all_prefixes = b"|".join(re.escape(p) for p in prefixes)
+ return re.compile(b"(?<![\\w\\-_/])([\\w\\-_]*?)(%s)([\\w\\-_/]*)" % all_prefixes)
+
+
+def utf8_paths_to_single_binary_regex(prefixes):
+ """Create a (binary) regex that matches any input path in utf8"""
+ return _byte_strings_to_single_binary_regex(p.encode("utf-8") for p in prefixes)
+
+
+def filter_identity_mappings(prefix_to_prefix):
+ """Drop mappings that are not changed."""
+ # NOTE: we don't guard against the following case:
+ # [/abc/def -> /abc/def, /abc -> /x] *will* be simplified to
+ # [/abc -> /x], meaning that after this simplification /abc/def will be
+ # mapped to /x/def instead of /abc/def. This should not be a problem.
+ return OrderedDict((k, v) for (k, v) in prefix_to_prefix.items() if k != v)
+
+
+class PrefixReplacer:
+ """Base class for applying a prefix to prefix map
+ to a list of binaries or text files.
+ Child classes implement _apply_to_file to do the
+ actual work, which is different when it comes to
+ binaries and text files."""
+
+ def __init__(self, prefix_to_prefix: Dict[bytes, bytes]):
+ """
+ Arguments:
+
+ prefix_to_prefix (OrderedDict):
+
+ A ordered mapping from prefix to prefix. The order is
+ relevant to support substring fallbacks, for example
+ [("/first/sub", "/x"), ("/first", "/y")] will ensure
+ /first/sub is matched and replaced before /first.
+ """
+ self.prefix_to_prefix = filter_identity_mappings(prefix_to_prefix)
+
+ @property
+ def is_noop(self) -> bool:
+ """Returns true when the prefix to prefix map
+ is mapping everything to the same location (identity)
+ or there are no prefixes to replace."""
+ return not bool(self.prefix_to_prefix)
+
+ def apply(self, filenames: list):
+ if self.is_noop:
+ return
+ for filename in filenames:
+ self.apply_to_filename(filename)
+
+ def apply_to_filename(self, filename):
+ if self.is_noop:
+ return
+ with open(filename, "rb+") as f:
+ self.apply_to_file(f)
+
+ def apply_to_file(self, f):
+ if self.is_noop:
+ return
+ self._apply_to_file(f)
+
+
+class TextFilePrefixReplacer(PrefixReplacer):
+ """This class applies prefix to prefix mappings for relocation
+ on text files.
+
+ Note that UTF-8 encoding is assumed."""
+
+ def __init__(self, prefix_to_prefix: Dict[bytes, bytes]):
+ """
+ prefix_to_prefix (OrderedDict): OrderedDictionary where the keys are
+ bytes representing the old prefixes and the values are the new.
+ """
+ super().__init__(prefix_to_prefix)
+ # Single regex for all paths.
+ self.regex = _byte_strings_to_single_binary_regex(self.prefix_to_prefix.keys())
+
+ @classmethod
+ def from_strings_or_bytes(
+ cls, prefix_to_prefix: Dict[Prefix, Prefix]
+ ) -> "TextFilePrefixReplacer":
+ """Create a TextFilePrefixReplacer from an ordered prefix to prefix map."""
+ return cls(_prefix_to_prefix_as_bytes(prefix_to_prefix))
+
+ def _apply_to_file(self, f):
+ """Text replacement implementation simply reads the entire file
+ in memory and applies the combined regex."""
+ replacement = lambda m: m.group(1) + self.prefix_to_prefix[m.group(2)] + m.group(3)
+ data = f.read()
+ new_data = re.sub(self.regex, replacement, data)
+ if id(data) == id(new_data):
+ return
+ f.seek(0)
+ f.write(new_data)
+ f.truncate()
+
+
+class BinaryFilePrefixReplacer(PrefixReplacer):
+ def __init__(self, prefix_to_prefix, suffix_safety_size=7):
+ """
+ prefix_to_prefix (OrderedDict): OrderedDictionary where the keys are
+ bytes representing the old prefixes and the values are the new
+ suffix_safety_size (int): in case of null terminated strings, what size
+ of the suffix should remain to avoid aliasing issues?
+ """
+ assert suffix_safety_size >= 0
+ super().__init__(prefix_to_prefix)
+ self.suffix_safety_size = suffix_safety_size
+ self.regex = self.binary_text_regex(self.prefix_to_prefix.keys(), suffix_safety_size)
+
+ @classmethod
+ def binary_text_regex(cls, binary_prefixes, suffix_safety_size=7):
+ """
+ Create a regex that looks for exact matches of prefixes, and also tries to
+ match a C-string type null terminator in a small lookahead window.
+
+ Arguments:
+ binary_prefixes (list): List of byte strings of prefixes to match
+ suffix_safety_size (int): Sizeof the lookahed for null-terminated string.
+
+ Returns: compiled regex
+ """
+ return re.compile(
+ b"("
+ + b"|".join(re.escape(p) for p in binary_prefixes)
+ + b")([^\0]{0,%d}\0)?" % suffix_safety_size
+ )
+
+ @classmethod
+ def from_strings_or_bytes(
+ cls, prefix_to_prefix: Dict[Prefix, Prefix], suffix_safety_size: int = 7
+ ) -> "BinaryFilePrefixReplacer":
+ """Create a BinaryFilePrefixReplacer from an ordered prefix to prefix map.
+
+ Arguments:
+ prefix_to_prefix (OrderedDict): Ordered mapping of prefix to prefix.
+ suffix_safety_size (int): Number of bytes to retain at the end of a C-string
+ to avoid binary string-aliasing issues.
+ """
+ return cls(_prefix_to_prefix_as_bytes(prefix_to_prefix), suffix_safety_size)
+
+ def _apply_to_file(self, f):
+ """
+ Given a file opened in rb+ mode, apply the string replacements as
+ specified by an ordered dictionary of prefix to prefix mappings. This
+ method takes special care of null-terminated C-strings. C-string constants
+ are problematic because compilers and linkers optimize readonly strings for
+ space by aliasing those that share a common suffix (only suffix since all
+ of them are null terminated). See https://github.com/spack/spack/pull/31739
+ and https://github.com/spack/spack/pull/32253 for details. Our logic matches
+ the original prefix with a ``suffix_safety_size + 1`` lookahead for null bytes.
+ If no null terminator is found, we simply pad with leading /, assuming that
+ it's a long C-string; the full C-string after replacement has a large suffix
+ in common with its original value.
+ If there *is* a null terminator we can do the same as long as the replacement
+ has a sufficiently long common suffix with the original prefix.
+ As a last resort when the replacement does not have a long enough common suffix,
+ we can try to shorten the string, but this only works if the new length is
+ sufficiently short (typically the case when going from large padding -> normal path)
+ If the replacement string is longer, or all of the above fails, we error out.
+
+ Arguments:
+ f: file opened in rb+ mode
+ """
+ assert f.tell() == 0
+
+ # We *could* read binary data in chunks to avoid loading all in memory,
+ # but it's nasty to deal with matches across boundaries, so let's stick to
+ # something simple.
+
+ for match in self.regex.finditer(f.read()):
+ # The matching prefix (old) and its replacement (new)
+ old = match.group(1)
+ new = self.prefix_to_prefix[old]
+
+ # Did we find a trailing null within a N + 1 bytes window after the prefix?
+ null_terminated = match.end(0) > match.end(1)
+
+ # Suffix string length, excluding the null byte
+ # Only makes sense if null_terminated
+ suffix_strlen = match.end(0) - match.end(1) - 1
+
+ # How many bytes are we shrinking our string?
+ bytes_shorter = len(old) - len(new)
+
+ # We can't make strings larger.
+ if bytes_shorter < 0:
+ raise CannotGrowString(old, new)
+
+ # If we don't know whether this is a null terminated C-string (we're looking
+ # only N + 1 bytes ahead), or if it is and we have a common suffix, we can
+ # simply pad with leading dir separators.
+ elif (
+ not null_terminated
+ or suffix_strlen >= self.suffix_safety_size # == is enough, but let's be defensive
+ or old[-self.suffix_safety_size + suffix_strlen :]
+ == new[-self.suffix_safety_size + suffix_strlen :]
+ ):
+ replacement = b"/" * bytes_shorter + new
+
+ # If it *was* null terminated, all that matters is that we can leave N bytes
+ # of old suffix in place. Note that > is required since we also insert an
+ # additional null terminator.
+ elif bytes_shorter > self.suffix_safety_size:
+ replacement = new + match.group(2) # includes the trailing null
+
+ # Otherwise... we can't :(
+ else:
+ raise CannotShrinkCString(old, new, match.group()[:-1])
+
+ f.seek(match.start())
+ f.write(replacement)
+
+
+class BinaryStringReplacementError(spack.error.SpackError):
+ def __init__(self, file_path, old_len, new_len):
+ """The size of the file changed after binary path substitution
+
+ Args:
+ file_path (str): file with changing size
+ old_len (str): original length of the file
+ new_len (str): length of the file after substitution
+ """
+ super(BinaryStringReplacementError, self).__init__(
+ "Doing a binary string replacement in %s failed.\n"
+ "The size of the file changed from %s to %s\n"
+ "when it should have remanined the same." % (file_path, old_len, new_len)
+ )
+
+
+class BinaryTextReplaceError(spack.error.SpackError):
+ def __init__(self, msg):
+ msg += (
+ " To fix this, compile with more padding "
+ "(config:install_tree:padded_length), or install to a shorter prefix."
+ )
+ super(BinaryTextReplaceError, self).__init__(msg)
+
+
+class CannotGrowString(BinaryTextReplaceError):
+ def __init__(self, old, new):
+ msg = "Cannot replace {!r} with {!r} because the new prefix is longer.".format(old, new)
+ super(CannotGrowString, self).__init__(msg)
+
+
+class CannotShrinkCString(BinaryTextReplaceError):
+ def __init__(self, old, new, full_old_string):
+ # Just interpolate binary string to not risk issues with invalid
+ # unicode, which would be really bad user experience: error in error.
+ # We have no clue if we actually deal with a real C-string nor what
+ # encoding it has.
+ msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
+ old, new, full_old_string
+ )
+ super(CannotShrinkCString, self).__init__(msg)
diff --git a/lib/spack/spack/rewiring.py b/lib/spack/spack/rewiring.py
index 71cc8ff371..359a36fbf9 100644
--- a/lib/spack/spack/rewiring.py
+++ b/lib/spack/spack/rewiring.py
@@ -70,7 +70,7 @@ def rewire_node(spec, explicit):
for rel_path in manifest.get("text_to_relocate", [])
]
if text_to_relocate:
- relocate.unsafe_relocate_text(files=text_to_relocate, prefixes=prefix_to_prefix)
+ relocate.relocate_text(files=text_to_relocate, prefixes=prefix_to_prefix)
bins_to_relocate = [
os.path.join(tempdir, spec.dag_hash(), rel_path)
@@ -97,7 +97,7 @@ def rewire_node(spec, explicit):
spec.build_spec.prefix,
spec.prefix,
)
- relocate.unsafe_relocate_text_bin(binaries=bins_to_relocate, prefixes=prefix_to_prefix)
+ relocate.relocate_text_bin(binaries=bins_to_relocate, prefixes=prefix_to_prefix)
# Copy package into place, except for spec.json (because spec.json
# describes the old spec and not the new spliced spec).
shutil.copytree(
diff --git a/lib/spack/spack/test/packaging.py b/lib/spack/spack/test/packaging.py
index 0b7e55f78d..16a5753e60 100644
--- a/lib/spack/spack/test/packaging.py
+++ b/lib/spack/spack/test/packaging.py
@@ -36,7 +36,7 @@ from spack.relocate import (
needs_binary_relocation,
needs_text_relocation,
relocate_links,
- unsafe_relocate_text,
+ relocate_text,
)
from spack.spec import Spec
@@ -190,7 +190,7 @@ echo $PATH"""
@pytest.mark.usefixtures("install_mockery")
-def test_unsafe_relocate_text(tmpdir):
+def test_relocate_text(tmpdir):
spec = Spec("trivial-install-test-package")
spec.concretize()
with tmpdir.as_cwd():
@@ -203,7 +203,7 @@ def test_unsafe_relocate_text(tmpdir):
filenames = [filename]
new_dir = "/opt/rh/devtoolset/"
# Singleton dict doesn't matter if Ordered
- unsafe_relocate_text(filenames, {old_dir: new_dir})
+ relocate_text(filenames, {old_dir: new_dir})
with open(filename, "r") as script:
for line in script:
assert new_dir in line
diff --git a/lib/spack/spack/test/relocate.py b/lib/spack/spack/test/relocate.py
index 97d24eeb8a..86f83102eb 100644
--- a/lib/spack/spack/test/relocate.py
+++ b/lib/spack/spack/test/relocate.py
@@ -2,13 +2,11 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
-import io
import os
import os.path
import re
import shutil
import sys
-from collections import OrderedDict
import pytest
@@ -18,11 +16,11 @@ import spack.concretize
import spack.paths
import spack.platforms
import spack.relocate
+import spack.relocate_text as relocate_text
import spack.spec
import spack.store
import spack.tengine
import spack.util.executable
-from spack.relocate import utf8_path_to_binary_regex, utf8_paths_to_single_binary_regex
pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="Tests fail on Windows")
@@ -269,7 +267,7 @@ def test_set_elf_rpaths_warning(mock_patchelf):
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
@skip_unless_linux
-def test_replace_prefix_bin(binary_with_rpaths, prefix_like):
+def test_relocate_text_bin(binary_with_rpaths, prefix_like):
prefix = "/usr/" + prefix_like
prefix_bytes = prefix.encode("utf-8")
new_prefix = "/foo/" + prefix_like
@@ -278,7 +276,7 @@ def test_replace_prefix_bin(binary_with_rpaths, prefix_like):
executable = binary_with_rpaths(rpaths=[prefix + "/lib", prefix + "/lib64"])
# Relocate the RPATHs
- spack.relocate._replace_prefix_bin(str(executable), {prefix_bytes: new_prefix_bytes})
+ spack.relocate.relocate_text_bin([str(executable)], {prefix_bytes: new_prefix_bytes})
# Some compilers add rpaths so ensure changes included in final result
assert "%s/lib:%s/lib64" % (new_prefix, new_prefix) in rpaths_for(executable)
@@ -349,7 +347,7 @@ def test_make_elf_binaries_relative(binary_with_rpaths, copy_binary, prefix_tmpd
@pytest.mark.requires_executables("patchelf", "strings", "file", "gcc")
@skip_unless_linux
-def test_relocate_text_bin(binary_with_rpaths, copy_binary, prefix_tmpdir):
+def test_relocate_text_bin_with_message(binary_with_rpaths, copy_binary, prefix_tmpdir):
orig_binary = binary_with_rpaths(
rpaths=[
str(prefix_tmpdir.mkdir("lib")),
@@ -368,7 +366,7 @@ def test_relocate_text_bin(binary_with_rpaths, copy_binary, prefix_tmpdir):
orig_path_bytes = str(orig_binary.dirpath()).encode("utf-8")
new_path_bytes = str(new_binary.dirpath()).encode("utf-8")
- spack.relocate.unsafe_relocate_text_bin([str(new_binary)], {orig_path_bytes: new_path_bytes})
+ spack.relocate.relocate_text_bin([str(new_binary)], {orig_path_bytes: new_path_bytes})
# Check original directory is not there anymore and it was
# substituted with the new one
@@ -382,8 +380,8 @@ def test_relocate_text_bin_raise_if_new_prefix_is_longer(tmpdir):
fpath = str(tmpdir.join("fakebin"))
with open(fpath, "w") as f:
f.write("/short")
- with pytest.raises(spack.relocate.BinaryTextReplaceError):
- spack.relocate.unsafe_relocate_text_bin([fpath], {short_prefix: long_prefix})
+ with pytest.raises(relocate_text.BinaryTextReplaceError):
+ spack.relocate.relocate_text_bin([fpath], {short_prefix: long_prefix})
@pytest.mark.requires_executables("install_name_tool", "file", "cc")
@@ -438,227 +436,3 @@ def test_fixup_macos_rpaths(make_dylib, make_object_file):
# (this is a corner case for GCC installation)
(root, filename) = make_object_file()
assert not fixup_rpath(root, filename)
-
-
-def test_text_relocation_regex_is_safe():
- # Test whether prefix regex is properly escaped
- string = b"This does not match /a/, but this does: /[a-z]/."
- assert utf8_path_to_binary_regex("/[a-z]/").search(string).group(0) == b"/[a-z]/"
-
-
-def test_utf8_paths_to_single_binary_regex():
- regex = utf8_paths_to_single_binary_regex(["/first/path", "/second/path", "/safe/[a-z]"])
- # Match nothing
- assert not regex.search(b"text /neither/first/path text /the/second/path text")
-
- # Match first
- string = b"contains both /first/path/subdir and /second/path/sub"
- assert regex.search(string).group(0) == b"/first/path/subdir"
-
- # Match second
- string = b"contains both /not/first/path/subdir but /second/path/subdir"
- assert regex.search(string).group(0) == b"/second/path/subdir"
-
- # Match "unsafe" dir name
- string = b"don't match /safe/a/path but do match /safe/[a-z]/file"
- assert regex.search(string).group(0) == b"/safe/[a-z]/file"
-
-
-def test_ordered_replacement():
- # This tests whether binary text replacement respects order, so that
- # a long package prefix is replaced before a shorter sub-prefix like
- # the root of the spack store (as a fallback).
- def replace_and_expect(prefix_map, before, after=None, suffix_safety_size=7):
- f = io.BytesIO(before)
- spack.relocate.apply_binary_replacements(f, OrderedDict(prefix_map), suffix_safety_size)
- f.seek(0)
- assert f.read() == after
-
- # The case of having a non-null terminated common suffix.
- replace_and_expect(
- [
- (b"/old-spack/opt/specific-package", b"/first/specific-package"),
- (b"/old-spack/opt", b"/sec/spack/opt"),
- ],
- b"Binary with /old-spack/opt/specific-package and /old-spack/opt",
- b"Binary with /////////first/specific-package and /sec/spack/opt",
- suffix_safety_size=7,
- )
-
- # The case of having a direct null terminated common suffix.
- replace_and_expect(
- [
- (b"/old-spack/opt/specific-package", b"/first/specific-package"),
- (b"/old-spack/opt", b"/sec/spack/opt"),
- ],
- b"Binary with /old-spack/opt/specific-package\0 and /old-spack/opt\0",
- b"Binary with /////////first/specific-package\0 and /sec/spack/opt\0",
- suffix_safety_size=7,
- )
-
- # Testing the order of operations (not null terminated, long enough common suffix)
- replace_and_expect(
- [
- (b"/old-spack/opt", b"/s/spack/opt"),
- (b"/old-spack/opt/specific-package", b"/first/specific-package"),
- ],
- b"Binary with /old-spack/opt/specific-package and /old-spack/opt",
- b"Binary with ///s/spack/opt/specific-package and ///s/spack/opt",
- suffix_safety_size=7,
- )
-
- # Testing the order of operations (null terminated, long enough common suffix)
- replace_and_expect(
- [
- (b"/old-spack/opt", b"/s/spack/opt"),
- (b"/old-spack/opt/specific-package", b"/first/specific-package"),
- ],
- b"Binary with /old-spack/opt/specific-package\0 and /old-spack/opt\0",
- b"Binary with ///s/spack/opt/specific-package\0 and ///s/spack/opt\0",
- suffix_safety_size=7,
- )
-
- # Null terminated within the lookahead window, common suffix long enough
- replace_and_expect(
- [(b"/old-spack/opt/specific-package", b"/opt/specific-XXXXage")],
- b"Binary with /old-spack/opt/specific-package/sub\0 data",
- b"Binary with ///////////opt/specific-XXXXage/sub\0 data",
- suffix_safety_size=7,
- )
-
- # Null terminated within the lookahead window, common suffix too short, but
- # shortening is enough to spare more than 7 bytes of old suffix.
- replace_and_expect(
- [(b"/old-spack/opt/specific-package", b"/opt/specific-XXXXXge")],
- b"Binary with /old-spack/opt/specific-package/sub\0 data",
- b"Binary with /opt/specific-XXXXXge/sub\0ckage/sub\0 data", # ckage/sub = 9 bytes
- suffix_safety_size=7,
- )
-
- # Null terminated within the lookahead window, common suffix too short,
- # shortening leaves exactly 7 suffix bytes untouched, amazing!
- replace_and_expect(
- [(b"/old-spack/opt/specific-package", b"/spack/specific-XXXXXge")],
- b"Binary with /old-spack/opt/specific-package/sub\0 data",
- b"Binary with /spack/specific-XXXXXge/sub\0age/sub\0 data", # age/sub = 7 bytes
- suffix_safety_size=7,
- )
-
- # Null terminated within the lookahead window, common suffix too short,
- # shortening doesn't leave space for 7 bytes, sad!
- error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
- b"/old-spack/opt/specific-package",
- b"/snacks/specific-XXXXXge",
- b"/old-spack/opt/specific-package/sub",
- )
- with pytest.raises(spack.relocate.CannotShrinkCString, match=error_msg):
- replace_and_expect(
- [(b"/old-spack/opt/specific-package", b"/snacks/specific-XXXXXge")],
- b"Binary with /old-spack/opt/specific-package/sub\0 data",
- # expect failure!
- suffix_safety_size=7,
- )
-
- # Check that it works when changing suffix_safety_size.
- replace_and_expect(
- [(b"/old-spack/opt/specific-package", b"/snacks/specific-XXXXXXe")],
- b"Binary with /old-spack/opt/specific-package/sub\0 data",
- b"Binary with /snacks/specific-XXXXXXe/sub\0ge/sub\0 data",
- suffix_safety_size=6,
- )
-
- # Finally check the case of no shortening but a long enough common suffix.
- replace_and_expect(
- [(b"pkg-gwixwaalgczp6", b"pkg-zkesfralgczp6")],
- b"Binary with pkg-gwixwaalgczp6/config\0 data",
- b"Binary with pkg-zkesfralgczp6/config\0 data",
- suffix_safety_size=7,
- )
-
- # Too short matching suffix, identical string length
- error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
- b"pkg-gwixwaxlgczp6",
- b"pkg-zkesfrzlgczp6",
- b"pkg-gwixwaxlgczp6",
- )
- with pytest.raises(spack.relocate.CannotShrinkCString, match=error_msg):
- replace_and_expect(
- [(b"pkg-gwixwaxlgczp6", b"pkg-zkesfrzlgczp6")],
- b"Binary with pkg-gwixwaxlgczp6\0 data",
- # expect failure
- suffix_safety_size=7,
- )
-
- # Finally, make sure that the regex is not greedily finding the LAST null byte
- # it should find the first null byte in the window. In this test we put one null
- # at a distance where we cant keep a long enough suffix, and one where we can,
- # so we should expect failure when the first null is used.
- error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
- b"pkg-abcdef",
- b"pkg-xyzabc",
- b"pkg-abcdef",
- )
- with pytest.raises(spack.relocate.CannotShrinkCString, match=error_msg):
- replace_and_expect(
- [(b"pkg-abcdef", b"pkg-xyzabc")],
- b"Binary with pkg-abcdef\0/xx\0", # def\0/xx is 7 bytes.
- # expect failure
- suffix_safety_size=7,
- )
-
-
-def test_inplace_text_replacement():
- def replace_and_expect(prefix_to_prefix, before: bytes, after: bytes):
- f = io.BytesIO(before)
- prefix_to_prefix = OrderedDict(prefix_to_prefix)
- regex = spack.relocate.byte_strings_to_single_binary_regex(prefix_to_prefix.keys())
- spack.relocate._replace_prefix_text_file(f, regex, prefix_to_prefix)
- f.seek(0)
- assert f.read() == after
-
- replace_and_expect(
- [
- (b"/first/prefix", b"/first-replacement/prefix"),
- (b"/second/prefix", b"/second-replacement/prefix"),
- ],
- b"Example: /first/prefix/subdir and /second/prefix/subdir",
- b"Example: /first-replacement/prefix/subdir and /second-replacement/prefix/subdir",
- )
-
- replace_and_expect(
- [
- (b"/replace/in/order", b"/first"),
- (b"/replace/in", b"/second"),
- (b"/replace", b"/third"),
- ],
- b"/replace/in/order/x /replace/in/y /replace/z",
- b"/first/x /second/y /third/z",
- )
-
- replace_and_expect(
- [
- (b"/replace", b"/third"),
- (b"/replace/in", b"/second"),
- (b"/replace/in/order", b"/first"),
- ],
- b"/replace/in/order/x /replace/in/y /replace/z",
- b"/third/in/order/x /third/in/y /third/z",
- )
-
- replace_and_expect(
- [(b"/my/prefix", b"/replacement")],
- b"/dont/replace/my/prefix #!/dont/replace/my/prefix",
- b"/dont/replace/my/prefix #!/dont/replace/my/prefix",
- )
-
- replace_and_expect(
- [(b"/my/prefix", b"/replacement")],
- b"Install path: /my/prefix.",
- b"Install path: /replacement.",
- )
-
- replace_and_expect(
- [(b"/my/prefix", b"/replacement")],
- b"#!/my/prefix",
- b"#!/replacement",
- )
diff --git a/lib/spack/spack/test/relocate_text.py b/lib/spack/spack/test/relocate_text.py
new file mode 100644
index 0000000000..9f97e7e83f
--- /dev/null
+++ b/lib/spack/spack/test/relocate_text.py
@@ -0,0 +1,247 @@
+# Copyright 2013-2022 Lawrence Livermore National Security, LLC and other
+# Spack Project Developers. See the top-level COPYRIGHT file for details.
+#
+# SPDX-License-Identifier: (Apache-2.0 OR MIT)
+import io
+from collections import OrderedDict
+
+import pytest
+
+import spack.relocate_text as relocate_text
+
+
+def test_text_relocation_regex_is_safe():
+ # Test whether prefix regex is properly escaped
+ string = b"This does not match /a/, but this does: /[a-z]/."
+ assert relocate_text.utf8_path_to_binary_regex("/[a-z]/").search(string).group(0) == b"/[a-z]/"
+
+
+def test_utf8_paths_to_single_binary_regex():
+ regex = relocate_text.utf8_paths_to_single_binary_regex(
+ ["/first/path", "/second/path", "/safe/[a-z]"]
+ )
+ # Match nothing
+ assert not regex.search(b"text /neither/first/path text /the/second/path text")
+
+ # Match first
+ string = b"contains both /first/path/subdir and /second/path/sub"
+ assert regex.search(string).group(0) == b"/first/path/subdir"
+
+ # Match second
+ string = b"contains both /not/first/path/subdir but /second/path/subdir"
+ assert regex.search(string).group(0) == b"/second/path/subdir"
+
+ # Match "unsafe" dir name
+ string = b"don't match /safe/a/path but do match /safe/[a-z]/file"
+ assert regex.search(string).group(0) == b"/safe/[a-z]/file"
+
+
+def test_ordered_replacement():
+ # This tests whether binary text replacement respects order, so that
+ # a long package prefix is replaced before a shorter sub-prefix like
+ # the root of the spack store (as a fallback).
+ def replace_and_expect(prefix_map, before, after=None, suffix_safety_size=7):
+ f = io.BytesIO(before)
+ relocater = relocate_text.BinaryFilePrefixReplacer(
+ OrderedDict(prefix_map), suffix_safety_size
+ )
+ relocater.apply_to_file(f)
+ f.seek(0)
+ assert f.read() == after
+
+ # The case of having a non-null terminated common suffix.
+ replace_and_expect(
+ [
+ (b"/old-spack/opt/specific-package", b"/first/specific-package"),
+ (b"/old-spack/opt", b"/sec/spack/opt"),
+ ],
+ b"Binary with /old-spack/opt/specific-package and /old-spack/opt",
+ b"Binary with /////////first/specific-package and /sec/spack/opt",
+ suffix_safety_size=7,
+ )
+
+ # The case of having a direct null terminated common suffix.
+ replace_and_expect(
+ [
+ (b"/old-spack/opt/specific-package", b"/first/specific-package"),
+ (b"/old-spack/opt", b"/sec/spack/opt"),
+ ],
+ b"Binary with /old-spack/opt/specific-package\0 and /old-spack/opt\0",
+ b"Binary with /////////first/specific-package\0 and /sec/spack/opt\0",
+ suffix_safety_size=7,
+ )
+
+ # Testing the order of operations (not null terminated, long enough common suffix)
+ replace_and_expect(
+ [
+ (b"/old-spack/opt", b"/s/spack/opt"),
+ (b"/old-spack/opt/specific-package", b"/first/specific-package"),
+ ],
+ b"Binary with /old-spack/opt/specific-package and /old-spack/opt",
+ b"Binary with ///s/spack/opt/specific-package and ///s/spack/opt",
+ suffix_safety_size=7,
+ )
+
+ # Testing the order of operations (null terminated, long enough common suffix)
+ replace_and_expect(
+ [
+ (b"/old-spack/opt", b"/s/spack/opt"),
+ (b"/old-spack/opt/specific-package", b"/first/specific-package"),
+ ],
+ b"Binary with /old-spack/opt/specific-package\0 and /old-spack/opt\0",
+ b"Binary with ///s/spack/opt/specific-package\0 and ///s/spack/opt\0",
+ suffix_safety_size=7,
+ )
+
+ # Null terminated within the lookahead window, common suffix long enough
+ replace_and_expect(
+ [(b"/old-spack/opt/specific-package", b"/opt/specific-XXXXage")],
+ b"Binary with /old-spack/opt/specific-package/sub\0 data",
+ b"Binary with ///////////opt/specific-XXXXage/sub\0 data",
+ suffix_safety_size=7,
+ )
+
+ # Null terminated within the lookahead window, common suffix too short, but
+ # shortening is enough to spare more than 7 bytes of old suffix.
+ replace_and_expect(
+ [(b"/old-spack/opt/specific-package", b"/opt/specific-XXXXXge")],
+ b"Binary with /old-spack/opt/specific-package/sub\0 data",
+ b"Binary with /opt/specific-XXXXXge/sub\0ckage/sub\0 data", # ckage/sub = 9 bytes
+ suffix_safety_size=7,
+ )
+
+ # Null terminated within the lookahead window, common suffix too short,
+ # shortening leaves exactly 7 suffix bytes untouched, amazing!
+ replace_and_expect(
+ [(b"/old-spack/opt/specific-package", b"/spack/specific-XXXXXge")],
+ b"Binary with /old-spack/opt/specific-package/sub\0 data",
+ b"Binary with /spack/specific-XXXXXge/sub\0age/sub\0 data", # age/sub = 7 bytes
+ suffix_safety_size=7,
+ )
+
+ # Null terminated within the lookahead window, common suffix too short,
+ # shortening doesn't leave space for 7 bytes, sad!
+ error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
+ b"/old-spack/opt/specific-package",
+ b"/snacks/specific-XXXXXge",
+ b"/old-spack/opt/specific-package/sub",
+ )
+ with pytest.raises(relocate_text.CannotShrinkCString, match=error_msg):
+ replace_and_expect(
+ [(b"/old-spack/opt/specific-package", b"/snacks/specific-XXXXXge")],
+ b"Binary with /old-spack/opt/specific-package/sub\0 data",
+ # expect failure!
+ suffix_safety_size=7,
+ )
+
+ # Check that it works when changing suffix_safety_size.
+ replace_and_expect(
+ [(b"/old-spack/opt/specific-package", b"/snacks/specific-XXXXXXe")],
+ b"Binary with /old-spack/opt/specific-package/sub\0 data",
+ b"Binary with /snacks/specific-XXXXXXe/sub\0ge/sub\0 data",
+ suffix_safety_size=6,
+ )
+
+ # Finally check the case of no shortening but a long enough common suffix.
+ replace_and_expect(
+ [(b"pkg-gwixwaalgczp6", b"pkg-zkesfralgczp6")],
+ b"Binary with pkg-gwixwaalgczp6/config\0 data",
+ b"Binary with pkg-zkesfralgczp6/config\0 data",
+ suffix_safety_size=7,
+ )
+
+ # Too short matching suffix, identical string length
+ error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
+ b"pkg-gwixwaxlgczp6",
+ b"pkg-zkesfrzlgczp6",
+ b"pkg-gwixwaxlgczp6",
+ )
+ with pytest.raises(relocate_text.CannotShrinkCString, match=error_msg):
+ replace_and_expect(
+ [(b"pkg-gwixwaxlgczp6", b"pkg-zkesfrzlgczp6")],
+ b"Binary with pkg-gwixwaxlgczp6\0 data",
+ # expect failure
+ suffix_safety_size=7,
+ )
+
+ # Finally, make sure that the regex is not greedily finding the LAST null byte
+ # it should find the first null byte in the window. In this test we put one null
+ # at a distance where we cant keep a long enough suffix, and one where we can,
+ # so we should expect failure when the first null is used.
+ error_msg = "Cannot replace {!r} with {!r} in the C-string {!r}.".format(
+ b"pkg-abcdef",
+ b"pkg-xyzabc",
+ b"pkg-abcdef",
+ )
+ with pytest.raises(relocate_text.CannotShrinkCString, match=error_msg):
+ replace_and_expect(
+ [(b"pkg-abcdef", b"pkg-xyzabc")],
+ b"Binary with pkg-abcdef\0/xx\0", # def\0/xx is 7 bytes.
+ # expect failure
+ suffix_safety_size=7,
+ )
+
+
+def test_inplace_text_replacement():
+ def replace_and_expect(prefix_to_prefix, before: bytes, after: bytes):
+ f = io.BytesIO(before)
+ replacer = relocate_text.TextFilePrefixReplacer(OrderedDict(prefix_to_prefix))
+ replacer.apply_to_file(f)
+ f.seek(0)
+ assert f.read() == after
+
+ replace_and_expect(
+ [
+ (b"/first/prefix", b"/first-replacement/prefix"),
+ (b"/second/prefix", b"/second-replacement/prefix"),
+ ],
+ b"Example: /first/prefix/subdir and /second/prefix/subdir",
+ b"Example: /first-replacement/prefix/subdir and /second-replacement/prefix/subdir",
+ )
+
+ replace_and_expect(
+ [
+ (b"/replace/in/order", b"/first"),
+ (b"/replace/in", b"/second"),
+ (b"/replace", b"/third"),
+ ],
+ b"/replace/in/order/x /replace/in/y /replace/z",
+ b"/first/x /second/y /third/z",
+ )
+
+ replace_and_expect(
+ [
+ (b"/replace", b"/third"),
+ (b"/replace/in", b"/second"),
+ (b"/replace/in/order", b"/first"),
+ ],
+ b"/replace/in/order/x /replace/in/y /replace/z",
+ b"/third/in/order/x /third/in/y /third/z",
+ )
+
+ replace_and_expect(
+ [(b"/my/prefix", b"/replacement")],
+ b"/dont/replace/my/prefix #!/dont/replace/my/prefix",
+ b"/dont/replace/my/prefix #!/dont/replace/my/prefix",
+ )
+
+ replace_and_expect(
+ [(b"/my/prefix", b"/replacement")],
+ b"Install path: /my/prefix.",
+ b"Install path: /replacement.",
+ )
+
+ replace_and_expect(
+ [(b"/my/prefix", b"/replacement")],
+ b"#!/my/prefix",
+ b"#!/replacement",
+ )
+
+
+def test_relocate_text_filters_redundant_entries():
+ # Test that we're filtering identical old / new paths, since that's a waste.
+ mapping = OrderedDict([("/hello", "/hello"), ("/world", "/world")])
+ replacer_1 = relocate_text.BinaryFilePrefixReplacer.from_strings_or_bytes(mapping)
+ replacer_2 = relocate_text.TextFilePrefixReplacer.from_strings_or_bytes(mapping)
+ assert not replacer_1.prefix_to_prefix
+ assert not replacer_2.prefix_to_prefix