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