1import collections 2import io 3import pathlib 4import struct 5import subprocess 6 7import pretend 8import pytest 9 10from packaging import _musllinux 11from packaging._musllinux import ( 12 _get_musl_version, 13 _MuslVersion, 14 _parse_ld_musl_from_elf, 15 _parse_musl_version, 16) 17 18MUSL_AMD64 = "musl libc (x86_64)\nVersion 1.2.2\n" 19MUSL_I386 = "musl libc (i386)\nVersion 1.2.1\n" 20MUSL_AARCH64 = "musl libc (aarch64)\nVersion 1.1.24\n" 21MUSL_INVALID = "musl libc (invalid)\n" 22MUSL_UNKNOWN = "musl libc (unknown)\nVersion unknown\n" 23 24MUSL_DIR = pathlib.Path(__file__).with_name("musllinux").resolve() 25 26BIN_GLIBC_X86_64 = MUSL_DIR.joinpath("glibc-x86_64") 27BIN_MUSL_X86_64 = MUSL_DIR.joinpath("musl-x86_64") 28BIN_MUSL_I386 = MUSL_DIR.joinpath("musl-i386") 29BIN_MUSL_AARCH64 = MUSL_DIR.joinpath("musl-aarch64") 30 31LD_MUSL_X86_64 = "/lib/ld-musl-x86_64.so.1" 32LD_MUSL_I386 = "/lib/ld-musl-i386.so.1" 33LD_MUSL_AARCH64 = "/lib/ld-musl-aarch64.so.1" 34 35 36@pytest.fixture(autouse=True) 37def clear_lru_cache(): 38 yield 39 _get_musl_version.cache_clear() 40 41 42@pytest.mark.parametrize( 43 "output, version", 44 [ 45 (MUSL_AMD64, _MuslVersion(1, 2)), 46 (MUSL_I386, _MuslVersion(1, 2)), 47 (MUSL_AARCH64, _MuslVersion(1, 1)), 48 (MUSL_INVALID, None), 49 (MUSL_UNKNOWN, None), 50 ], 51 ids=["amd64-1.2.2", "i386-1.2.1", "aarch64-1.1.24", "invalid", "unknown"], 52) 53def test_parse_musl_version(output, version): 54 assert _parse_musl_version(output) == version 55 56 57@pytest.mark.parametrize( 58 "executable, location", 59 [ 60 (BIN_GLIBC_X86_64, None), 61 (BIN_MUSL_X86_64, LD_MUSL_X86_64), 62 (BIN_MUSL_I386, LD_MUSL_I386), 63 (BIN_MUSL_AARCH64, LD_MUSL_AARCH64), 64 ], 65 ids=["glibc", "x86_64", "i386", "aarch64"], 66) 67def test_parse_ld_musl_from_elf(executable, location): 68 with executable.open("rb") as f: 69 assert _parse_ld_musl_from_elf(f) == location 70 71 72@pytest.mark.parametrize( 73 "data", 74 [ 75 # Too short for magic. 76 b"\0", 77 # Enough for magic, but not ELF. 78 b"#!/bin/bash" + b"\0" * 16, 79 # ELF, but unknown byte declaration. 80 b"\x7fELF\3" + b"\0" * 16, 81 ], 82 ids=["no-magic", "wrong-magic", "unknown-format"], 83) 84def test_parse_ld_musl_from_elf_invalid(data): 85 assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None 86 87 88@pytest.mark.parametrize( 89 "head", 90 [ 91 25, # Enough for magic, but not the section definitions. 92 58, # Enough for section definitions, but not the actual sections. 93 ], 94) 95def test_parse_ld_musl_from_elf_invalid_section(head): 96 data = BIN_MUSL_X86_64.read_bytes()[:head] 97 assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None 98 99 100def test_parse_ld_musl_from_elf_no_interpreter_section(): 101 with BIN_MUSL_X86_64.open("rb") as f: 102 data = f.read() 103 104 # Change all sections to *not* PT_INTERP. 105 unpacked = struct.unpack("16BHHIQQQIHHH", data[:58]) 106 *_, e_phoff, _, _, _, e_phentsize, e_phnum = unpacked 107 for i in range(e_phnum + 1): 108 sb = e_phoff + e_phentsize * i 109 se = sb + 56 110 section = struct.unpack("IIQQQQQQ", data[sb:se]) 111 data = data[:sb] + struct.pack("IIQQQQQQ", 0, *section[1:]) + data[se:] 112 113 assert _parse_ld_musl_from_elf(io.BytesIO(data)) is None 114 115 116@pytest.mark.parametrize( 117 "executable, output, version, ld_musl", 118 [ 119 (MUSL_DIR.joinpath("does-not-exist"), "error", None, None), 120 (BIN_GLIBC_X86_64, "error", None, None), 121 (BIN_MUSL_X86_64, MUSL_AMD64, _MuslVersion(1, 2), LD_MUSL_X86_64), 122 (BIN_MUSL_I386, MUSL_I386, _MuslVersion(1, 2), LD_MUSL_I386), 123 (BIN_MUSL_AARCH64, MUSL_AARCH64, _MuslVersion(1, 1), LD_MUSL_AARCH64), 124 ], 125 ids=["does-not-exist", "glibc", "x86_64", "i386", "aarch64"], 126) 127def test_get_musl_version(monkeypatch, executable, output, version, ld_musl): 128 def mock_run(*args, **kwargs): 129 return collections.namedtuple("Proc", "stderr")(output) 130 131 run_recorder = pretend.call_recorder(mock_run) 132 monkeypatch.setattr(_musllinux.subprocess, "run", run_recorder) 133 134 assert _get_musl_version(str(executable)) == version 135 136 if ld_musl is not None: 137 expected_calls = [ 138 pretend.call( 139 [ld_musl], 140 stderr=subprocess.PIPE, 141 universal_newlines=True, 142 ) 143 ] 144 else: 145 expected_calls = [] 146 assert run_recorder.calls == expected_calls 147