one more test on real sysiphus packets

This commit is contained in:
2026-08-17 04:48:19 +03:00
parent f72d345f0d
commit 452190b8c4
12 changed files with 578801 additions and 0 deletions
@@ -0,0 +1,385 @@
#!/usr/bin/env python3
"""Measure P(hash output bit = 1) over a line-oriented ASCII corpus."""
from __future__ import annotations
import argparse
import csv
import hashlib
import subprocess
import tempfile
from collections.abc import Sequence
from dataclasses import dataclass
from pathlib import Path
from statistics import fmean
from typing import TextIO
from plot_probability_map import ProbabilityRow, render_bit_probabilities
from probability_map import HASHES, HASH_FUNCS_DIR
ROOT = Path(__file__).resolve().parent
DEFAULT_OUTPUT_DIR = ROOT / "corpus_bit_distribution"
class DistributionError(RuntimeError):
"""Raised when a corpus or hash adapter cannot be processed."""
@dataclass(frozen=True)
class HashSpec:
bits: int
expression: str
HASH_SPECS = {
"jenkinsOAAT": HashSpec(32, "jenkins_oaat(word, length)"),
"xxh64": HashSpec(64, "xxh64(word, length, XXH64_SEED)"),
"t1ha2": HashSpec(64, "t1ha2_atonce(word, length, T1HA2_SEED)"),
}
@dataclass(frozen=True)
class Distribution:
hash_name: str
samples: int
bits: int
ones: tuple[int, ...]
def __post_init__(self) -> None:
if self.samples < 1:
raise ValueError("число образцов должно быть положительным")
if self.bits < 1 or len(self.ones) != self.bits:
raise ValueError("число счётчиков должно совпадать с шириной хэша")
if any(count < 0 or count > self.samples for count in self.ones):
raise ValueError("счётчик единиц должен находиться между 0 и samples")
@property
def probabilities(self) -> tuple[float, ...]:
return tuple(count / self.samples for count in self.ones)
def c_include_path(path: Path) -> str:
"""Return a C string-safe absolute include path."""
return str(path.resolve()).replace("\\", "\\\\").replace('"', '\\"')
def make_adapter_source(source: Path, spec: HashSpec) -> str:
"""Build a C program that aggregates bit counts using an existing hash source."""
return f'''#define _POSIX_C_SOURCE 200809L
#define main arsv_original_hash_cli_main
#include "{c_include_path(source)}"
#undef main
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
int main(void)
{{
char *buffer = NULL;
size_t capacity = 0;
uint64_t samples = UINT64_C(0);
uint64_t ones[{spec.bits}] = {{UINT64_C(0)}};
ssize_t bytes_read;
while ((bytes_read = getline(&buffer, &capacity, stdin)) >= 0) {{
size_t length = (size_t)bytes_read;
const unsigned char *word = (const unsigned char *)buffer;
while (length > 0U && is_ascii_trailing_space(word[length - 1U])) {{
--length;
}}
if (length == 0U) {{
fprintf(stderr, "corpus contains an empty line\\n");
free(buffer);
return EXIT_FAILURE;
}}
for (size_t index = 0; index < length; ++index) {{
if (word[index] > 0x7fU) {{
fprintf(stderr, "corpus must contain ASCII characters only\\n");
free(buffer);
return EXIT_FAILURE;
}}
}}
if (samples == UINT64_MAX) {{
fprintf(stderr, "sample counter overflow\\n");
free(buffer);
return EXIT_FAILURE;
}}
uint64_t hash = (uint64_t)({spec.expression});
for (unsigned int bit = 0U; bit < {spec.bits}U; ++bit) {{
ones[bit] += (hash >> bit) & UINT64_C(1);
}}
++samples;
}}
if (ferror(stdin)) {{
fprintf(stderr, "failed to read corpus\\n");
free(buffer);
return EXIT_FAILURE;
}}
free(buffer);
printf("%" PRIu64 ",{spec.bits}", samples);
for (unsigned int bit = 0U; bit < {spec.bits}U; ++bit) {{
printf(",%" PRIu64, ones[bit]);
}}
putchar('\\n');
return EXIT_SUCCESS;
}}
'''
def measure_hash(
hash_name: str,
corpus: Path,
hash_funcs_dir: Path = HASH_FUNCS_DIR,
) -> Distribution:
"""Compile a batch adapter and count set output bits for every corpus line."""
try:
spec = HASH_SPECS[hash_name]
except KeyError as error:
supported = ", ".join(HASH_SPECS)
raise DistributionError(
f"нет batch-адаптера для {hash_name!r}; доступны: {supported}"
) from error
source = hash_funcs_dir / hash_name / "bin_hash.c"
if not source.is_file():
raise DistributionError(f"не найден исходник хэша: {source}")
if not corpus.is_file():
raise DistributionError(f"не найден corpus: {corpus}")
with tempfile.TemporaryDirectory(prefix=f"arsv-{hash_name}-") as temporary:
root = Path(temporary)
adapter_source = root / "batch_counts.c"
adapter = root / "batch_counts"
adapter_source.write_text(make_adapter_source(source, spec), encoding="utf-8")
command = [
"cc",
"-std=c11",
"-O3",
"-Wall",
"-Wextra",
"-Wpedantic",
"-Werror",
str(adapter_source),
"-o",
str(adapter),
]
compiled = subprocess.run(command, text=True, capture_output=True, check=False)
if compiled.returncode != 0:
details = compiled.stderr.strip() or compiled.stdout.strip()
raise DistributionError(f"не удалось собрать адаптер {hash_name}: {details}")
try:
with corpus.open("rb") as stream:
measured = subprocess.run(
[str(adapter)],
stdin=stream,
text=True,
capture_output=True,
check=False,
)
except OSError as error:
raise DistributionError(f"не удалось прочитать {corpus}: {error}") from error
if measured.returncode != 0:
details = measured.stderr.strip() or measured.stdout.strip()
raise DistributionError(f"batch-прогон {hash_name} завершился ошибкой: {details}")
fields = measured.stdout.strip().split(",")
if len(fields) != spec.bits + 2:
raise DistributionError(
f"batch-прогон {hash_name} вернул {len(fields)} полей вместо {spec.bits + 2}"
)
try:
samples = int(fields[0])
bits = int(fields[1])
ones = tuple(int(value) for value in fields[2:])
except ValueError as error:
raise DistributionError(
f"batch-прогон {hash_name} вернул некорректные счётчики"
) from error
if bits != spec.bits:
raise DistributionError(
f"batch-прогон {hash_name} сообщил ширину {bits} вместо {spec.bits}"
)
try:
return Distribution(hash_name, samples, bits, ones)
except ValueError as error:
raise DistributionError(f"некорректный результат {hash_name}: {error}") from error
def write_distribution_csv(stream: TextIO, distribution: Distribution) -> None:
"""Write bit counts and probabilities from LSB to MSB."""
writer = csv.writer(stream, lineterminator="\n")
writer.writerow(["bit", "ones", "zeros", "probability", "deviation_from_0.5"])
for bit, (ones, probability) in enumerate(
zip(distribution.ones, distribution.probabilities, strict=True)
):
writer.writerow(
[
bit,
ones,
distribution.samples - ones,
f"{probability:.9f}",
f"{abs(probability - 0.5):.9f}",
]
)
def write_summary_csv(stream: TextIO, distributions: Sequence[Distribution]) -> None:
writer = csv.writer(stream, lineterminator="\n")
writer.writerow(
[
"hash",
"samples",
"bits",
"mean_probability",
"mean_abs_deviation",
"max_abs_deviation",
"min_probability",
"max_probability",
]
)
for distribution in distributions:
probabilities = distribution.probabilities
deviations = [abs(value - 0.5) for value in probabilities]
writer.writerow(
[
distribution.hash_name,
distribution.samples,
distribution.bits,
f"{fmean(probabilities):.9f}",
f"{fmean(deviations):.9f}",
f"{max(deviations):.9f}",
f"{min(probabilities):.9f}",
f"{max(probabilities):.9f}",
]
)
def sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def write_manifest(
path: Path,
corpus: Path,
distributions: Sequence[Distribution],
) -> None:
samples = {distribution.samples for distribution in distributions}
if len(samples) != 1:
raise DistributionError("хэши обработали разное число строк corpus")
content = "\n".join(
[
f"corpus={corpus}",
f"corpus_bytes={corpus.stat().st_size}",
f"corpus_sha256={sha256_file(corpus)}",
f"samples={samples.pop()}",
f"hashes={','.join(item.hash_name for item in distributions)}",
"metric=P(hash_output_bit=1)",
"bit_order=bit_0_is_LSB",
"input_format=one_ASCII_symbol_per_line",
"",
]
)
path.write_text(content, encoding="utf-8")
def run(
corpus: Path,
output_directory: Path,
hash_names: Sequence[str],
) -> list[Distribution]:
"""Measure all requested hashes and create CSV, manifest, and one PNG."""
names = list(dict.fromkeys(hash_names))
if not names:
raise DistributionError("нужно указать хотя бы один хэш")
distributions = [measure_hash(name, corpus) for name in names]
sample_counts = {item.samples for item in distributions}
if len(sample_counts) != 1:
raise DistributionError("хэши обработали разное число строк corpus")
output_directory.mkdir(parents=True, exist_ok=True)
for distribution in distributions:
path = output_directory / f"{distribution.hash_name}.csv"
with path.open("w", encoding="utf-8", newline="") as stream:
write_distribution_csv(stream, distribution)
with (output_directory / "summary.csv").open(
"w", encoding="utf-8", newline=""
) as stream:
write_summary_csv(stream, distributions)
write_manifest(output_directory / "manifest.txt", corpus, distributions)
rows = [
ProbabilityRow(
operation=item.hash_name,
pairs=item.samples,
probabilities=list(item.probabilities),
)
for item in distributions
]
render_bit_probabilities(
rows,
output_directory / "bit_distribution.png",
"C++ Provides corpus: P(hash output bit = 1)",
)
return distributions
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Считает прямое распределение единиц по выходным битам хэша "
"для ASCII corpus (одна строка — одно значение). bit_0 — младший бит."
)
)
parser.add_argument("corpus", type=Path, help="текстовый corpus")
parser.add_argument(
"--output",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help=f"отдельный каталог результатов (по умолчанию: {DEFAULT_OUTPUT_DIR})",
)
parser.add_argument(
"--hash",
dest="hashes",
action="append",
help="проверить указанный хэш; можно повторять (по умолчанию все)",
)
return parser
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
arguments = parser.parse_args(argv)
try:
distributions = run(
corpus=arguments.corpus,
output_directory=arguments.output,
hash_names=arguments.hashes or HASHES,
)
except (DistributionError, OSError, ValueError) as error:
parser.error(str(error))
for distribution in distributions:
probabilities = distribution.probabilities
mean_deviation = fmean(abs(value - 0.5) for value in probabilities)
print(
f"hash={distribution.hash_name} samples={distribution.samples} "
f"bits={distribution.bits} mean_abs_deviation={mean_deviation:.9f}"
)
print(f"wrote {arguments.output}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+426
View File
@@ -0,0 +1,426 @@
#!/usr/bin/env python3
"""Extract ALT RPM Provides symbol names into a hash-testing corpus.
Local RPM paths are processed directly. Other positional arguments are resolved
as binary package names through the ALT Repository Database (RDB).
"""
from __future__ import annotations
import argparse
import hashlib
import json
import os
import shutil
import stat
import subprocess
import sys
import tempfile
import urllib.parse
import urllib.request
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Sequence, cast
RDB_BASE = "https://rdb.altlinux.org/api"
NEWC_MAGICS = {b"070701", b"070702"}
NEWC_HEADER_SIZE = 110
COPY_CHUNK_SIZE = 1024 * 1024
MAX_CPIO_NAME_SIZE = 1024 * 1024
class CorpusError(RuntimeError):
"""A user-facing extraction error."""
@dataclass(frozen=True)
class PackageInput:
label: str
path: Path
@dataclass(frozen=True)
class PackageSymbols:
label: str
elf_files: int
symbols: frozenset[str]
def parse_arguments(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Extract canonical ALT rpm-build Provided ELF symbols from multiple "
"RPM packages, one symbol per output line."
)
)
parser.add_argument(
"packages",
nargs="+",
help="local .rpm path or ALT binary package name",
)
parser.add_argument(
"-o",
"--output",
default="-",
help="output file (default: stdout)",
)
parser.add_argument(
"--cpp-only",
action="store_true",
help="keep only Itanium C++ ABI mangled names beginning with _Z",
)
parser.add_argument(
"--branch",
default="p11",
help="ALT branch for package-name resolution (default: p11)",
)
parser.add_argument(
"--arch",
default="x86_64",
help="binary package architecture (default: x86_64)",
)
parser.add_argument(
"--rpm2cpio",
default=shutil.which("rpm2cpio") or "rpm2cpio",
help=argparse.SUPPRESS,
)
parser.add_argument(
"--provided-symbols",
default=(
"/usr/lib/rpm/provided_symbols"
if Path("/usr/lib/rpm/provided_symbols").is_file()
else shutil.which("provided_symbols") or "provided_symbols"
),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--file-command",
default=shutil.which("file") or "file",
help=argparse.SUPPRESS,
)
return parser.parse_args(argv)
def read_json(url: str) -> dict[str, object]:
try:
with urllib.request.urlopen(url, timeout=60) as response:
return json.load(response)
except (OSError, ValueError, json.JSONDecodeError) as error:
raise CorpusError(f"failed to read ALT RDB response from {url}: {error}") from error
def download_package(name: str, branch: str, arch: str, directory: Path) -> PackageInput:
query = urllib.parse.urlencode({"branch": branch, "name": name, "arch": arch})
metadata_url = f"{RDB_BASE}/site/pkghash_by_binary_name?{query}"
metadata = read_json(metadata_url)
package_hash = metadata.get("pkghash")
if not isinstance(package_hash, str) or not package_hash:
raise CorpusError(f"ALT RDB did not return a package hash for {name!r}")
download_url = (
f"{RDB_BASE}/site/package_downloads_bin/{package_hash}?"
f"{urllib.parse.urlencode({'branch': branch, 'arch': arch})}"
)
download_data = read_json(download_url)
downloads = download_data.get("downloads")
if not isinstance(downloads, list):
raise CorpusError(f"ALT RDB did not return downloads for {name!r}")
package_record: dict[str, object] | None = None
for architecture_record in downloads:
if not isinstance(architecture_record, dict):
continue
if architecture_record.get("arch") != arch:
continue
packages = architecture_record.get("packages")
if isinstance(packages, list):
for candidate in packages:
if isinstance(candidate, dict):
package_record = candidate
break
if package_record is not None:
break
if package_record is None:
raise CorpusError(f"ALT RDB has no {arch} RPM download for {name!r}")
filename = package_record.get("name")
url = package_record.get("url")
expected_md5 = package_record.get("md5")
if not isinstance(filename, str) or not filename.endswith(".rpm"):
raise CorpusError(f"ALT RDB returned an invalid RPM filename for {name!r}")
if not isinstance(url, str) or not url.startswith(("https://", "http://")):
raise CorpusError(f"ALT RDB returned an invalid RPM URL for {name!r}")
destination = directory / filename
temporary = destination.with_suffix(destination.suffix + ".part")
digest = hashlib.md5(usedforsecurity=False)
try:
with urllib.request.urlopen(url, timeout=180) as response, temporary.open("wb") as output:
while chunk := response.read(COPY_CHUNK_SIZE):
output.write(chunk)
digest.update(chunk)
except OSError as error:
temporary.unlink(missing_ok=True)
raise CorpusError(f"failed to download {name!r} from {url}: {error}") from error
if isinstance(expected_md5, str) and digest.hexdigest().lower() != expected_md5.lower():
temporary.unlink(missing_ok=True)
raise CorpusError(f"MD5 mismatch for downloaded package {name!r}")
temporary.replace(destination)
return PackageInput(f"{name} ({filename})", destination)
def resolve_packages(
specifications: Sequence[str], branch: str, arch: str, directory: Path
) -> list[PackageInput]:
resolved: list[PackageInput] = []
seen: set[tuple[str, str]] = set()
for specification in specifications:
candidate = Path(specification).expanduser()
if candidate.is_file():
package = PackageInput(candidate.name, candidate.resolve())
elif candidate.exists():
raise CorpusError(f"package input is not a regular file: {candidate}")
elif "/" in specification or specification.endswith(".rpm"):
raise CorpusError(f"RPM file does not exist: {candidate}")
else:
package = download_package(specification, branch, arch, directory)
identity = (package.label, str(package.path))
if identity not in seen:
resolved.append(package)
seen.add(identity)
return resolved
def read_exact(stream: BinaryIO, size: int, context: str) -> bytes:
chunks: list[bytes] = []
remaining = size
while remaining:
chunk = stream.read(remaining)
if not chunk:
raise CorpusError(f"truncated newc stream while reading {context}")
chunks.append(chunk)
remaining -= len(chunk)
return b"".join(chunks)
def discard_exact(stream: BinaryIO, size: int, context: str) -> None:
remaining = size
while remaining:
chunk = stream.read(min(remaining, COPY_CHUNK_SIZE))
if not chunk:
raise CorpusError(f"truncated newc stream while reading {context}")
remaining -= len(chunk)
def copy_exact(stream: BinaryIO, output: BinaryIO, size: int, context: str) -> None:
remaining = size
while remaining:
chunk = stream.read(min(remaining, COPY_CHUNK_SIZE))
if not chunk:
raise CorpusError(f"truncated newc stream while reading {context}")
output.write(chunk)
remaining -= len(chunk)
def parse_newc_header(header: bytes) -> tuple[int, int, int]:
if len(header) != NEWC_HEADER_SIZE or header[:6] not in NEWC_MAGICS:
raise CorpusError("rpm2cpio output is not a valid newc archive")
try:
fields = [int(header[6 + index * 8 : 14 + index * 8], 16) for index in range(13)]
except ValueError as error:
raise CorpusError("newc header contains a non-hexadecimal field") from error
mode = fields[1]
file_size = fields[6]
name_size = fields[11]
if name_size < 1 or name_size > MAX_CPIO_NAME_SIZE:
raise CorpusError(f"invalid newc pathname size: {name_size}")
return mode, file_size, name_size
def extract_elf_members(stream: BinaryIO, directory: Path) -> list[Path]:
elf_files: list[Path] = []
member_index = 0
while True:
header = read_exact(stream, NEWC_HEADER_SIZE, "header")
mode, file_size, name_size = parse_newc_header(header)
raw_name = read_exact(stream, name_size, "pathname")
if raw_name[-1:] != b"\0":
raise CorpusError("newc pathname is not NUL-terminated")
discard_exact(stream, -(NEWC_HEADER_SIZE + name_size) % 4, "pathname padding")
if raw_name[:-1] == b"TRAILER!!!":
discard_exact(stream, file_size, "trailer data")
discard_exact(stream, -file_size % 4, "trailer padding")
break
member_index += 1
prefix_size = min(file_size, 4)
prefix = read_exact(stream, prefix_size, "member data")
remaining = file_size - prefix_size
if stat.S_ISREG(mode) and prefix == b"\x7fELF":
destination = directory / f"elf-{member_index:06d}"
with destination.open("wb") as output:
output.write(prefix)
copy_exact(stream, output, remaining, "ELF member data")
elf_files.append(destination)
else:
discard_exact(stream, remaining, "member data")
discard_exact(stream, -file_size % 4, "member padding")
return elf_files
def extract_package_symbols(
package: PackageInput,
rpm2cpio: str,
provided_symbols: str,
file_command: str,
directory: Path,
) -> PackageSymbols:
package_directory = directory / f"package-{len(list(directory.iterdir())):04d}"
package_directory.mkdir()
process = subprocess.Popen(
[rpm2cpio, str(package.path)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
assert process.stdout is not None
assert process.stderr is not None
try:
extracted_elf_files = extract_elf_members(
cast(BinaryIO, process.stdout), package_directory
)
except Exception:
process.kill()
process.communicate()
raise
finally:
process.stdout.close()
stderr = process.stderr.read().decode(errors="replace")
return_code = process.wait()
if return_code != 0:
raise CorpusError(
f"rpm2cpio failed for {package.label} with status {return_code}: {stderr.strip()}"
)
elf_files: list[Path] = []
environment = os.environ.copy()
environment["LC_ALL"] = "C"
for path in extracted_elf_files:
result = subprocess.run(
[file_command, "--brief", "--", str(path)],
capture_output=True,
text=True,
env=environment,
check=False,
)
if result.returncode != 0:
raise CorpusError(
f"file failed for {package.label} with status "
f"{result.returncode}: {result.stderr.strip()}"
)
description = f" {result.stdout.strip()} "
if (
" ELF " in description
and " shared object, " in description
and " shared object, no machine, " not in description
):
elf_files.append(path)
if not elf_files:
return PackageSymbols(package.label, 0, frozenset())
result = subprocess.run(
[provided_symbols, *(str(path) for path in elf_files)],
capture_output=True,
text=True,
env=environment,
check=False,
)
if result.returncode != 0:
raise CorpusError(
f"provided_symbols failed for {package.label} with status "
f"{result.returncode}: {result.stderr.strip()}"
)
symbols = frozenset(line for line in result.stdout.splitlines() if line)
return PackageSymbols(package.label, len(elf_files), symbols)
def render_symbols(symbols: set[str]) -> str:
if not symbols:
return ""
return "\n".join(sorted(symbols)) + "\n"
def write_output(path: str, content: str) -> None:
if path == "-":
sys.stdout.write(content)
return
destination = Path(path).expanduser()
destination.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
"w", encoding="utf-8", dir=destination.parent, delete=False
) as temporary:
temporary.write(content)
temporary_path = Path(temporary.name)
temporary_path.replace(destination)
def run(arguments: argparse.Namespace) -> int:
with tempfile.TemporaryDirectory(prefix="provided-symbols-") as temporary_name:
temporary = Path(temporary_name)
download_directory = temporary / "downloads"
extraction_directory = temporary / "extract"
download_directory.mkdir()
extraction_directory.mkdir()
packages = resolve_packages(
arguments.packages, arguments.branch, arguments.arch, download_directory
)
combined: set[str] = set()
total_elf_files = 0
for package in packages:
package_symbols = extract_package_symbols(
package,
arguments.rpm2cpio,
arguments.provided_symbols,
arguments.file_command,
extraction_directory,
)
selected = {
symbol
for symbol in package_symbols.symbols
if not arguments.cpp_only or symbol.startswith("_Z")
}
combined.update(selected)
total_elf_files += package_symbols.elf_files
print(
f"package={package_symbols.label} "
f"elf_files={package_symbols.elf_files} "
f"symbols={len(package_symbols.symbols)} selected={len(selected)}",
file=sys.stderr,
)
write_output(arguments.output, render_symbols(combined))
print(
f"packages={len(packages)} elf_files={total_elf_files} "
f"unique_symbols={len(combined)} output={arguments.output}",
file=sys.stderr,
)
return 0
def main(argv: Sequence[str] | None = None) -> int:
try:
return run(parse_arguments(argv))
except (CorpusError, OSError, subprocess.SubprocessError) as error:
print(f"error: {error}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,22 @@
branch=p11
arch=x86_64
filter=Itanium C++ ABI mangled names beginning with _Z
extractor=/usr/lib/rpm/provided_symbols
selection=largest x86_64 C++ candidates found by Provides set-string length in apt-cache dumpavail
candidate=slicer set_length=509743
candidate=libmlir22.1 set_length=303303
candidate=libmlir21.1 set_length=284616
candidate=libmlir18.1 set_length=204305
candidate=libmlir20.1 set_length=182470
package=slicer (slicer-5.10.0-alt1.x86_64.rpm) elf_files=227 symbols=380667 selected=380530
package=libmlir22.1 (libmlir22.1-22.1.1-alt0.1.x86_64.rpm) elf_files=9 symbols=146036 selected=145767
package=libmlir21.1 (libmlir21.1-21.1.5-alt0.1.x86_64.rpm) elf_files=8 symbols=136102 selected=135854
package=libmlir20.1 (libmlir20.1-20.1.8-alt0.4.x86_64.rpm) elf_files=8 symbols=90904 selected=90656
package=libmlir18.1 (libmlir18.1-18.1.8-alt0.2.x86_64.rpm) elf_files=7 symbols=103099 selected=102835
packages=5 elf_files=259 unique_symbols=577509 output=cpp_provides_p11.txt
similar_lcp12=575206 share=0.996012
similar_lcp24=548917 share=0.950491
median_max_lcp=61
max_lcp=1018
sha256=bf8e760bdf4d52b42040cff4625de73988d8dd6b517566efd186c71e3382753d
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -0,0 +1,33 @@
bit,ones,zeros,probability,deviation_from_0.5
0,287790,289719,0.498329896,0.001670104
1,288092,289417,0.498852832,0.001147168
2,288750,288759,0.499992208,0.000007792
3,288546,288963,0.499638967,0.000361033
4,288778,288731,0.500040692,0.000040692
5,289060,288449,0.500528996,0.000528996
6,288814,288695,0.500103029,0.000103029
7,288788,288721,0.500058008,0.000058008
8,288712,288797,0.499926408,0.000073592
9,288209,289300,0.499055426,0.000944574
10,289080,288429,0.500563628,0.000563628
11,288964,288545,0.500362765,0.000362765
12,289040,288469,0.500494365,0.000494365
13,289175,288334,0.500728127,0.000728127
14,288883,288626,0.500222507,0.000222507
15,288908,288601,0.500265797,0.000265797
16,288259,289250,0.499142005,0.000857995
17,288493,289016,0.499547193,0.000452807
18,288101,289408,0.498868416,0.001131584
19,289164,288345,0.500709080,0.000709080
20,288817,288692,0.500108223,0.000108223
21,288723,288786,0.499945455,0.000054545
22,288577,288932,0.499692645,0.000307355
23,288523,288986,0.499599140,0.000400860
24,288625,288884,0.499775761,0.000224239
25,288128,289381,0.498915168,0.001084832
26,288821,288688,0.500115150,0.000115150
27,288762,288747,0.500012987,0.000012987
28,289539,287970,0.501358420,0.001358420
29,288493,289016,0.499547193,0.000452807
30,288614,288895,0.499756714,0.000243286
31,288681,288828,0.499872729,0.000127271
1 bit ones zeros probability deviation_from_0.5
2 0 287790 289719 0.498329896 0.001670104
3 1 288092 289417 0.498852832 0.001147168
4 2 288750 288759 0.499992208 0.000007792
5 3 288546 288963 0.499638967 0.000361033
6 4 288778 288731 0.500040692 0.000040692
7 5 289060 288449 0.500528996 0.000528996
8 6 288814 288695 0.500103029 0.000103029
9 7 288788 288721 0.500058008 0.000058008
10 8 288712 288797 0.499926408 0.000073592
11 9 288209 289300 0.499055426 0.000944574
12 10 289080 288429 0.500563628 0.000563628
13 11 288964 288545 0.500362765 0.000362765
14 12 289040 288469 0.500494365 0.000494365
15 13 289175 288334 0.500728127 0.000728127
16 14 288883 288626 0.500222507 0.000222507
17 15 288908 288601 0.500265797 0.000265797
18 16 288259 289250 0.499142005 0.000857995
19 17 288493 289016 0.499547193 0.000452807
20 18 288101 289408 0.498868416 0.001131584
21 19 289164 288345 0.500709080 0.000709080
22 20 288817 288692 0.500108223 0.000108223
23 21 288723 288786 0.499945455 0.000054545
24 22 288577 288932 0.499692645 0.000307355
25 23 288523 288986 0.499599140 0.000400860
26 24 288625 288884 0.499775761 0.000224239
27 25 288128 289381 0.498915168 0.001084832
28 26 288821 288688 0.500115150 0.000115150
29 27 288762 288747 0.500012987 0.000012987
30 28 289539 287970 0.501358420 0.001358420
31 29 288493 289016 0.499547193 0.000452807
32 30 288614 288895 0.499756714 0.000243286
33 31 288681 288828 0.499872729 0.000127271
@@ -0,0 +1,8 @@
corpus=input_symbols/cpp_provides_p11.txt
corpus_bytes=54941449
corpus_sha256=bf8e760bdf4d52b42040cff4625de73988d8dd6b517566efd186c71e3382753d
samples=577509
hashes=jenkinsOAAT,xxh64,t1ha2
metric=P(hash_output_bit=1)
bit_order=bit_0_is_LSB
input_format=one_ASCII_symbol_per_line
@@ -0,0 +1,4 @@
hash,samples,bits,mean_probability,mean_abs_deviation,max_abs_deviation,min_probability,max_probability
jenkinsOAAT,577509,32,0.499879060,0.000475425,0.001670104,0.498329896,0.501358420
xxh64,577509,64,0.499871404,0.000497665,0.001704735,0.498295265,0.501221626
t1ha2,577509,64,0.500008550,0.000532513,0.001876161,0.498123839,0.501256257
1 hash samples bits mean_probability mean_abs_deviation max_abs_deviation min_probability max_probability
2 jenkinsOAAT 577509 32 0.499879060 0.000475425 0.001670104 0.498329896 0.501358420
3 xxh64 577509 64 0.499871404 0.000497665 0.001704735 0.498295265 0.501221626
4 t1ha2 577509 64 0.500008550 0.000532513 0.001876161 0.498123839 0.501256257
@@ -0,0 +1,65 @@
bit,ones,zeros,probability,deviation_from_0.5
0,288524,288985,0.499600872,0.000399128
1,289273,288236,0.500897822,0.000897822
2,289241,288268,0.500842411,0.000842411
3,289050,288459,0.500511680,0.000511680
4,288827,288682,0.500125539,0.000125539
5,289341,288168,0.501015569,0.001015569
6,288570,288939,0.499680524,0.000319476
7,289480,288029,0.501256257,0.001256257
8,289469,288040,0.501237210,0.001237210
9,289359,288150,0.501046737,0.001046737
10,288340,289169,0.499282262,0.000717738
11,288850,288659,0.500165365,0.000165365
12,288401,289108,0.499387888,0.000612112
13,288828,288681,0.500127271,0.000127271
14,288809,288700,0.500094371,0.000094371
15,288437,289072,0.499450225,0.000549775
16,287671,289838,0.498123839,0.001876161
17,288776,288733,0.500037229,0.000037229
18,288058,289451,0.498793958,0.001206042
19,289300,288209,0.500944574,0.000944574
20,288721,288788,0.499941992,0.000058008
21,288403,289106,0.499391351,0.000608649
22,288749,288760,0.499990476,0.000009524
23,289382,288127,0.501086563,0.001086563
24,288503,289006,0.499564509,0.000435491
25,289237,288272,0.500835485,0.000835485
26,288918,288591,0.500283112,0.000283112
27,288889,288620,0.500232897,0.000232897
28,288205,289304,0.499048500,0.000951500
29,288637,288872,0.499796540,0.000203460
30,289047,288462,0.500506486,0.000506486
31,288479,289030,0.499522951,0.000477049
32,289147,288362,0.500679643,0.000679643
33,288429,289080,0.499436372,0.000563628
34,288675,288834,0.499862340,0.000137660
35,288817,288692,0.500108223,0.000108223
36,288302,289207,0.499216462,0.000783538
37,288197,289312,0.499034647,0.000965353
38,289472,288037,0.501242405,0.001242405
39,289181,288328,0.500738517,0.000738517
40,289046,288463,0.500504754,0.000504754
41,288910,288599,0.500269260,0.000269260
42,288608,288901,0.499746324,0.000253676
43,288745,288764,0.499983550,0.000016450
44,288429,289080,0.499436372,0.000563628
45,288738,288771,0.499971429,0.000028571
46,288425,289084,0.499429446,0.000570554
47,288857,288652,0.500177486,0.000177486
48,288795,288714,0.500070129,0.000070129
49,288155,289354,0.498961921,0.001038079
50,288639,288870,0.499800003,0.000199997
51,289013,288496,0.500447612,0.000447612
52,288717,288792,0.499935066,0.000064934
53,289130,288379,0.500650206,0.000650206
54,288936,288573,0.500314281,0.000314281
55,288904,288605,0.500258870,0.000258870
56,288616,288893,0.499760177,0.000239823
57,288873,288636,0.500205192,0.000205192
58,288340,289169,0.499282262,0.000717738
59,288507,289002,0.499571435,0.000428565
60,288986,288523,0.500400860,0.000400860
61,288162,289347,0.498974042,0.001025958
62,288354,289155,0.499306504,0.000693496
63,288725,288784,0.499948919,0.000051081
1 bit ones zeros probability deviation_from_0.5
2 0 288524 288985 0.499600872 0.000399128
3 1 289273 288236 0.500897822 0.000897822
4 2 289241 288268 0.500842411 0.000842411
5 3 289050 288459 0.500511680 0.000511680
6 4 288827 288682 0.500125539 0.000125539
7 5 289341 288168 0.501015569 0.001015569
8 6 288570 288939 0.499680524 0.000319476
9 7 289480 288029 0.501256257 0.001256257
10 8 289469 288040 0.501237210 0.001237210
11 9 289359 288150 0.501046737 0.001046737
12 10 288340 289169 0.499282262 0.000717738
13 11 288850 288659 0.500165365 0.000165365
14 12 288401 289108 0.499387888 0.000612112
15 13 288828 288681 0.500127271 0.000127271
16 14 288809 288700 0.500094371 0.000094371
17 15 288437 289072 0.499450225 0.000549775
18 16 287671 289838 0.498123839 0.001876161
19 17 288776 288733 0.500037229 0.000037229
20 18 288058 289451 0.498793958 0.001206042
21 19 289300 288209 0.500944574 0.000944574
22 20 288721 288788 0.499941992 0.000058008
23 21 288403 289106 0.499391351 0.000608649
24 22 288749 288760 0.499990476 0.000009524
25 23 289382 288127 0.501086563 0.001086563
26 24 288503 289006 0.499564509 0.000435491
27 25 289237 288272 0.500835485 0.000835485
28 26 288918 288591 0.500283112 0.000283112
29 27 288889 288620 0.500232897 0.000232897
30 28 288205 289304 0.499048500 0.000951500
31 29 288637 288872 0.499796540 0.000203460
32 30 289047 288462 0.500506486 0.000506486
33 31 288479 289030 0.499522951 0.000477049
34 32 289147 288362 0.500679643 0.000679643
35 33 288429 289080 0.499436372 0.000563628
36 34 288675 288834 0.499862340 0.000137660
37 35 288817 288692 0.500108223 0.000108223
38 36 288302 289207 0.499216462 0.000783538
39 37 288197 289312 0.499034647 0.000965353
40 38 289472 288037 0.501242405 0.001242405
41 39 289181 288328 0.500738517 0.000738517
42 40 289046 288463 0.500504754 0.000504754
43 41 288910 288599 0.500269260 0.000269260
44 42 288608 288901 0.499746324 0.000253676
45 43 288745 288764 0.499983550 0.000016450
46 44 288429 289080 0.499436372 0.000563628
47 45 288738 288771 0.499971429 0.000028571
48 46 288425 289084 0.499429446 0.000570554
49 47 288857 288652 0.500177486 0.000177486
50 48 288795 288714 0.500070129 0.000070129
51 49 288155 289354 0.498961921 0.001038079
52 50 288639 288870 0.499800003 0.000199997
53 51 289013 288496 0.500447612 0.000447612
54 52 288717 288792 0.499935066 0.000064934
55 53 289130 288379 0.500650206 0.000650206
56 54 288936 288573 0.500314281 0.000314281
57 55 288904 288605 0.500258870 0.000258870
58 56 288616 288893 0.499760177 0.000239823
59 57 288873 288636 0.500205192 0.000205192
60 58 288340 289169 0.499282262 0.000717738
61 59 288507 289002 0.499571435 0.000428565
62 60 288986 288523 0.500400860 0.000400860
63 61 288162 289347 0.498974042 0.001025958
64 62 288354 289155 0.499306504 0.000693496
65 63 288725 288784 0.499948919 0.000051081
@@ -0,0 +1,65 @@
bit,ones,zeros,probability,deviation_from_0.5
0,288778,288731,0.500040692,0.000040692
1,288503,289006,0.499564509,0.000435491
2,288868,288641,0.500196534,0.000196534
3,288638,288871,0.499798272,0.000201728
4,288821,288688,0.500115150,0.000115150
5,288588,288921,0.499711693,0.000288307
6,288882,288627,0.500220776,0.000220776
7,289460,288049,0.501221626,0.001221626
8,288866,288643,0.500193071,0.000193071
9,288162,289347,0.498974042,0.001025958
10,289120,288389,0.500632891,0.000632891
11,288842,288667,0.500151513,0.000151513
12,288456,289053,0.499483125,0.000516875
13,288410,289099,0.499403472,0.000596528
14,289202,288307,0.500774880,0.000774880
15,288850,288659,0.500165365,0.000165365
16,288499,289010,0.499557583,0.000442417
17,288163,289346,0.498975774,0.001024226
18,288220,289289,0.499074473,0.000925527
19,288892,288617,0.500238092,0.000238092
20,288695,288814,0.499896971,0.000103029
21,288797,288712,0.500073592,0.000073592
22,288795,288714,0.500070129,0.000070129
23,288188,289321,0.499019063,0.000980937
24,288864,288645,0.500189607,0.000189607
25,288653,288856,0.499824245,0.000175755
26,289025,288484,0.500468391,0.000468391
27,288750,288759,0.499992208,0.000007792
28,289267,288242,0.500887432,0.000887432
29,288448,289061,0.499469272,0.000530728
30,288742,288767,0.499978355,0.000021645
31,288247,289262,0.499121226,0.000878774
32,288629,288880,0.499782687,0.000217313
33,289449,288060,0.501202579,0.001202579
34,288761,288748,0.500011255,0.000011255
35,288208,289301,0.499053694,0.000946306
36,288308,289201,0.499226852,0.000773148
37,289073,288436,0.500551507,0.000551507
38,288394,289115,0.499375767,0.000624233
39,289142,288367,0.500670985,0.000670985
40,288574,288935,0.499687451,0.000312549
41,288209,289300,0.499055426,0.000944574
42,288292,289217,0.499199147,0.000800853
43,288781,288728,0.500045887,0.000045887
44,287770,289739,0.498295265,0.001704735
45,289014,288495,0.500449344,0.000449344
46,289209,288300,0.500787001,0.000787001
47,289366,288143,0.501058858,0.001058858
48,288445,289064,0.499464078,0.000535922
49,288384,289125,0.499358452,0.000641548
50,288509,289000,0.499574898,0.000425102
51,288599,288910,0.499730740,0.000269260
52,288771,288738,0.500028571,0.000028571
53,287887,289622,0.498497859,0.001502141
54,288871,288638,0.500201728,0.000201728
55,288294,289215,0.499202610,0.000797390
56,288557,288952,0.499658014,0.000341986
57,288416,289093,0.499413862,0.000586138
58,288674,288835,0.499860608,0.000139392
59,288644,288865,0.499808661,0.000191339
60,288679,288830,0.499869266,0.000130734
61,288828,288681,0.500127271,0.000127271
62,288905,288604,0.500260602,0.000260602
63,289202,288307,0.500774880,0.000774880
1 bit ones zeros probability deviation_from_0.5
2 0 288778 288731 0.500040692 0.000040692
3 1 288503 289006 0.499564509 0.000435491
4 2 288868 288641 0.500196534 0.000196534
5 3 288638 288871 0.499798272 0.000201728
6 4 288821 288688 0.500115150 0.000115150
7 5 288588 288921 0.499711693 0.000288307
8 6 288882 288627 0.500220776 0.000220776
9 7 289460 288049 0.501221626 0.001221626
10 8 288866 288643 0.500193071 0.000193071
11 9 288162 289347 0.498974042 0.001025958
12 10 289120 288389 0.500632891 0.000632891
13 11 288842 288667 0.500151513 0.000151513
14 12 288456 289053 0.499483125 0.000516875
15 13 288410 289099 0.499403472 0.000596528
16 14 289202 288307 0.500774880 0.000774880
17 15 288850 288659 0.500165365 0.000165365
18 16 288499 289010 0.499557583 0.000442417
19 17 288163 289346 0.498975774 0.001024226
20 18 288220 289289 0.499074473 0.000925527
21 19 288892 288617 0.500238092 0.000238092
22 20 288695 288814 0.499896971 0.000103029
23 21 288797 288712 0.500073592 0.000073592
24 22 288795 288714 0.500070129 0.000070129
25 23 288188 289321 0.499019063 0.000980937
26 24 288864 288645 0.500189607 0.000189607
27 25 288653 288856 0.499824245 0.000175755
28 26 289025 288484 0.500468391 0.000468391
29 27 288750 288759 0.499992208 0.000007792
30 28 289267 288242 0.500887432 0.000887432
31 29 288448 289061 0.499469272 0.000530728
32 30 288742 288767 0.499978355 0.000021645
33 31 288247 289262 0.499121226 0.000878774
34 32 288629 288880 0.499782687 0.000217313
35 33 289449 288060 0.501202579 0.001202579
36 34 288761 288748 0.500011255 0.000011255
37 35 288208 289301 0.499053694 0.000946306
38 36 288308 289201 0.499226852 0.000773148
39 37 289073 288436 0.500551507 0.000551507
40 38 288394 289115 0.499375767 0.000624233
41 39 289142 288367 0.500670985 0.000670985
42 40 288574 288935 0.499687451 0.000312549
43 41 288209 289300 0.499055426 0.000944574
44 42 288292 289217 0.499199147 0.000800853
45 43 288781 288728 0.500045887 0.000045887
46 44 287770 289739 0.498295265 0.001704735
47 45 289014 288495 0.500449344 0.000449344
48 46 289209 288300 0.500787001 0.000787001
49 47 289366 288143 0.501058858 0.001058858
50 48 288445 289064 0.499464078 0.000535922
51 49 288384 289125 0.499358452 0.000641548
52 50 288509 289000 0.499574898 0.000425102
53 51 288599 288910 0.499730740 0.000269260
54 52 288771 288738 0.500028571 0.000028571
55 53 287887 289622 0.498497859 0.001502141
56 54 288871 288638 0.500201728 0.000201728
57 55 288294 289215 0.499202610 0.000797390
58 56 288557 288952 0.499658014 0.000341986
59 57 288416 289093 0.499413862 0.000586138
60 58 288674 288835 0.499860608 0.000139392
61 59 288644 288865 0.499808661 0.000191339
62 60 288679 288830 0.499869266 0.000130734
63 61 288828 288681 0.500127271 0.000127271
64 62 288905 288604 0.500260602 0.000260602
65 63 289202 288307 0.500774880 0.000774880
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Behavior tests for corpus_bit_distribution.py."""
from __future__ import annotations
import csv
import io
import tempfile
import unittest
from pathlib import Path
from PIL import Image
import corpus_bit_distribution
import probability_map
class CorpusBitDistributionTests(unittest.TestCase):
def test_batch_counts_match_scalar_cli_for_every_hash(self) -> None:
words = ["a", "abc", "HashWord", "_ZN4llvm3fooEv"]
with tempfile.TemporaryDirectory() as temporary_directory:
corpus = Path(temporary_directory) / "corpus.txt"
corpus.write_text("".join(f"{word}\n" for word in words), encoding="ascii")
for hash_name in probability_map.HASHES:
with self.subTest(hash_name=hash_name):
distribution = corpus_bit_distribution.measure_hash(
hash_name,
corpus,
)
executable = probability_map.prepare_hash(hash_name)
scalar = [
probability_map.run_hash(executable, word)[0] for word in words
]
expected_ones = tuple(
sum((value >> bit) & 1 for value in scalar)
for bit in range(distribution.bits)
)
self.assertEqual(distribution.samples, len(words))
self.assertEqual(distribution.ones, expected_ones)
def test_writes_one_csv_row_per_lsb_first_bit(self) -> None:
distribution = corpus_bit_distribution.Distribution(
hash_name="tiny",
samples=4,
bits=3,
ones=(1, 2, 4),
)
stream = io.StringIO()
corpus_bit_distribution.write_distribution_csv(stream, distribution)
rows = list(csv.reader(io.StringIO(stream.getvalue())))
self.assertEqual(
rows[0],
["bit", "ones", "zeros", "probability", "deviation_from_0.5"],
)
self.assertEqual(rows[1], ["0", "1", "3", "0.250000000", "0.250000000"])
self.assertEqual(rows[2], ["1", "2", "2", "0.500000000", "0.000000000"])
self.assertEqual(rows[3], ["2", "4", "0", "1.000000000", "0.500000000"])
def test_run_creates_separate_csv_summary_manifest_and_png(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
root = Path(temporary_directory)
corpus = root / "corpus.txt"
corpus.write_text("a\nabc\nHashWord\n", encoding="ascii")
output = root / "results"
result = corpus_bit_distribution.run(
corpus=corpus,
output_directory=output,
hash_names=["jenkinsOAAT"],
)
self.assertEqual(result[0].samples, 3)
self.assertTrue((output / "jenkinsOAAT.csv").is_file())
self.assertTrue((output / "summary.csv").is_file())
self.assertTrue((output / "manifest.txt").is_file())
image_path = output / "bit_distribution.png"
self.assertTrue(image_path.is_file())
with Image.open(image_path) as image:
self.assertEqual(image.format, "PNG")
self.assertGreater(image.width, 300)
self.assertGreater(image.height, 200)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""Behavior tests for extract_provided_symbols.py."""
from __future__ import annotations
import stat
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "extract_provided_symbols.py"
def newc_entry(name: str, data: bytes, mode: int = stat.S_IFREG | 0o644) -> bytes:
encoded_name = name.encode("utf-8") + b"\0"
fields = (
1,
mode,
0,
0,
1,
0,
len(data),
0,
0,
0,
0,
len(encoded_name),
0,
)
header = b"070701" + b"".join(f"{value:08x}".encode() for value in fields)
record = header + encoded_name
record += b"\0" * (-len(record) % 4)
record += data
record += b"\0" * (-len(data) % 4)
return record
def newc_archive(entries: list[tuple[str, bytes, int]]) -> bytes:
payload = b"".join(newc_entry(*entry) for entry in entries)
return payload + newc_entry("TRAILER!!!", b"", 0)
class ExtractProvidedSymbolsTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary_directory = tempfile.TemporaryDirectory()
self.work = Path(self.temporary_directory.name)
self.rpm2cpio = self.work / "rpm2cpio"
self.provided_symbols = self.work / "provided_symbols"
self.file_command = self.work / "file"
self.rpm2cpio.write_text(
"#!/usr/bin/env python3\n"
"import pathlib, sys\n"
"sys.stdout.buffer.write(pathlib.Path(sys.argv[1]).read_bytes())\n"
)
self.provided_symbols.write_text(
"#!/usr/bin/env python3\n"
"import pathlib, sys\n"
"symbols = set()\n"
"for name in sys.argv[1:]:\n"
" symbols.update(pathlib.Path(name).read_bytes()[4:].decode().splitlines())\n"
"print(*sorted(symbols), sep='\\n')\n"
)
self.file_command.write_text(
"#!/usr/bin/env python3\n"
"import pathlib, sys\n"
"data = pathlib.Path(sys.argv[-1]).read_bytes()\n"
"kind = 'pie executable' if b'EXECUTABLE' in data else 'shared object'\n"
"print(f'ELF 64-bit LSB {kind}, x86-64, version 1 (SYSV)')\n"
)
self.rpm2cpio.chmod(0o755)
self.provided_symbols.chmod(0o755)
self.file_command.chmod(0o755)
def tearDown(self) -> None:
self.temporary_directory.cleanup()
def run_script(self, *arguments: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
sys.executable,
str(SCRIPT),
"--rpm2cpio",
str(self.rpm2cpio),
"--provided-symbols",
str(self.provided_symbols),
"--file-command",
str(self.file_command),
*arguments,
],
capture_output=True,
text=True,
check=False,
)
def test_combines_unique_cpp_symbols_from_multiple_packages(self) -> None:
first = self.work / "first.rpm"
second = self.work / "second.rpm"
output = self.work / "symbols.txt"
first.write_bytes(
newc_archive(
[
("./usr/lib64/libfirst.so", b"\x7fELF_ZN3Foo3barEv\nplain_c_symbol\n", stat.S_IFREG | 0o755),
("./usr/share/doc/readme", b"not ELF", stat.S_IFREG | 0o644),
("./usr/lib64/libalias.so", b"libfirst.so", stat.S_IFLNK | 0o777),
]
)
)
second.write_bytes(
newc_archive(
[
(
"./usr/lib64/libsecond.so",
b"\x7fELF_ZN3Foo3barEv\n_ZN3Foo3bazEv\nanother_c_symbol\n",
stat.S_IFREG | 0o755,
),
]
)
)
result = self.run_script(
"--cpp-only", "--output", str(output), str(first), str(second)
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
output.read_text().splitlines(),
["_ZN3Foo3barEv", "_ZN3Foo3bazEv"],
)
self.assertIn("package=first.rpm elf_files=1 symbols=2 selected=1", result.stderr)
self.assertIn("package=second.rpm elf_files=1 symbols=3 selected=2", result.stderr)
self.assertIn("unique_symbols=2", result.stderr)
self.assertEqual(result.stdout, "")
def test_without_cpp_filter_preserves_all_provided_symbols(self) -> None:
package = self.work / "all.rpm"
package.write_bytes(
newc_archive(
[
(
"./usr/lib64/liball.so",
b"\x7fELF_ZN3Foo3barEv\nplain_c_symbol\n",
stat.S_IFREG | 0o755,
)
]
)
)
result = self.run_script(str(package))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(), ["_ZN3Foo3barEv", "plain_c_symbol"])
def test_excludes_elf_executables_that_do_not_generate_library_provides(self) -> None:
package = self.work / "mixed.rpm"
package.write_bytes(
newc_archive(
[
(
"./usr/lib64/libmixed.so",
b"\x7fELF_ZN3Lib3runEv\n",
stat.S_IFREG | 0o755,
),
(
"./usr/bin/mixed",
b"\x7fELFEXECUTABLE\n_ZN4Exec3runEv\n",
stat.S_IFREG | 0o755,
),
]
)
)
result = self.run_script("--cpp-only", str(package))
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.splitlines(), ["_ZN3Lib3runEv"])
self.assertIn("elf_files=1", result.stderr)
def test_rejects_malformed_cpio_stream(self) -> None:
package = self.work / "broken.rpm"
package.write_bytes(b"not a newc archive")
result = self.run_script(str(package))
self.assertNotEqual(result.returncode, 0)
self.assertIn("newc", result.stderr)
if __name__ == "__main__":
unittest.main()