"""Force Deployment/Reporting URL scheme from https to http for lab proxy testing. Core/type0x01 build URLs with the cstring/CFString format ``https://%@``. daily.html plugin URLs are already ``http://[HOST_PLACEHOLDER]/...``. ``https://%@`` (len 10) is replaced with ``http://%@\\0`` (9 visible chars + pad) and matching CFString length fields are updated 10 → 9. Do **not** use a trailing slash in the format string — paths already start with ``/``, which produced ``https://host//api/...``. """ from __future__ import annotations import struct from _path_patch import _arm64e_target, _fat_slices, _parse_macho OLD = b"https://%@" # Previous mistaken same-length patch (caused host//path). LEGACY_SLASH = b"http://%@/" # 10-byte slot: 9-char format + NUL pad (original terminator becomes a second NUL). NEW_SLOT = b"http://%@\x00" NEW_VISIBLE = b"http://%@" def _patch_thin_scheme(data: bytes, *, expect_hits: int, label: str) -> bytes: macho = _parse_macho(data, label=label) base_subtype = macho.cpu_subtype & 0x00FFFFFF architecture = ( "arm64e" if base_subtype == 2 else "arm64" if base_subtype == 0 else "" ) if architecture not in {"arm64", "arm64e"}: raise SystemExit(f"{label}: unsupported CPU subtype {base_subtype}") cstring = macho.section("__TEXT", "__cstring") region = bytearray(data[cstring.offset : cstring.offset + cstring.size]) # Prefer migrating legacy slash form, else patch original https form. if region.count(LEGACY_SLASH) == expect_hits and region.count(OLD) == 0: source = LEGACY_SLASH elif region.count(OLD) == expect_hits: source = OLD elif ( region.count(NEW_VISIBLE) >= expect_hits and region.count(OLD) == 0 and region.count(LEGACY_SLASH) == 0 ): # Already patched to http://%@ (NUL-terminated). return data else: raise SystemExit( f"{label}: expected {expect_hits} {OLD!r} or {LEGACY_SLASH!r} in " f"__cstring; found https={region.count(OLD)} slash={region.count(LEGACY_SLASH)}" ) hits: list[int] = [] start = 0 while True: hit = region.find(source, start) if hit < 0: break hits.append(hit) start = hit + 1 if len(hits) != expect_hits: raise SystemExit( f"{label}: __cstring scheme hits={len(hits)}, expected {expect_hits}" ) for hit in hits: region[hit : hit + len(NEW_SLOT)] = NEW_SLOT buf = bytearray(data) buf[cstring.offset : cstring.offset + cstring.size] = region cfstring = macho.section("__DATA_CONST", "__cfstring") if cfstring.size % 32: raise SystemExit(f"{label}: __cfstring size is not record-aligned") patched_lengths = 0 for hit in hits: path_vmaddr = cstring.addr + hit for record_offset in range( cfstring.offset, cfstring.offset + cfstring.size, 32 ): raw = struct.unpack_from(" bytes: """Patch ``https://%@`` → ``http://%@`` (CFString length 9) in thin or fat dylibs. ``expect_hits`` is the total number of format strings across the whole file (2 for fat core, 1 for thin type0x01). """ slices = _fat_slices(data, label=label) if slices is None: return _patch_thin_scheme(data, expect_hits=expect_hits, label=label) if expect_hits % len(slices) != 0: raise SystemExit( f"{label}: expect_hits={expect_hits} not divisible by fat slices={len(slices)}" ) per_slice = expect_hits // len(slices) buf = bytearray(data) for index, slice_info in enumerate(slices): thin = bytes(buf[slice_info.offset : slice_info.offset + slice_info.size]) patched = _patch_thin_scheme( thin, expect_hits=per_slice, label=f"{label}[slice{index}]" ) if len(patched) != slice_info.size: raise SystemExit(f"{label}: fat slice size changed after scheme patch") buf[slice_info.offset : slice_info.offset + slice_info.size] = patched return bytes(buf)