diff --git a/src/machista1.0/Makefile.in b/src/machista1.0/Makefile.in index 82f3ea92d..96fcc85a5 100644 --- a/src/machista1.0/Makefile.in +++ b/src/machista1.0/Makefile.in @@ -50,7 +50,8 @@ clean:: rm -f ${SWIG_OBJS} ${PKG_INDEX} rm -f ${TESTS} \ tests/libmachista-test-dependency${SHLIB_SUFFIX} \ - tests/libmachista-test-lib${SHLIB_SUFFIX} + tests/libmachista-test-lib${SHLIB_SUFFIX} \ + tests/libmachista-test-archive.a rm -rf ${TESTS:%=%.dSYM} \ tests/libmachista-test-dependency${SHLIB_SUFFIX}.dSYM \ tests/libmachista-test-lib${SHLIB_SUFFIX}.dSYM @@ -61,9 +62,22 @@ distclean:: test:: ${TESTS} ${TESTS} -tests/libmachista-test: tests/libmachista-test.c libmachista.h libmachista.o hashmap.o tests/libmachista-test-lib${SHLIB_SUFFIX} +tests/libmachista-test: tests/libmachista-test.c libmachista.h libmachista.o hashmap.o tests/libmachista-test-lib${SHLIB_SUFFIX} tests/libmachista-test-archive.a $(CC) $(CFLAGS) -D_POSIX_SOURCE -o $@ -I. $< libmachista.o hashmap.o +# A static archive (e.g. a *.a library) of more than one host-arch object, used +# to check that libmachista parses archives and reports each arch only once. +tests/libmachista-test-archive.a: tests/empty.c +ifeq (darwin,@OS_PLATFORM@) + $(CC) $(CFLAGS) -c $< -o tests/libmachista-test-archive-1.o + $(CC) $(CFLAGS) -c $< -o tests/libmachista-test-archive-2.o + rm -f $@ + ar rcs $@ tests/libmachista-test-archive-1.o tests/libmachista-test-archive-2.o + rm -f tests/libmachista-test-archive-1.o tests/libmachista-test-archive-2.o +else + touch $@ +endif + # The tests for this library need a universal lib; we used to just use # /usr/lib/libSystem.B.dylib, but Apple has removed that with macOS 11, so now # we need a different alternative. diff --git a/src/machista1.0/libmachista.c b/src/machista1.0/libmachista.c index b758d1400..9e0d6f19d 100644 --- a/src/machista1.0/libmachista.c +++ b/src/machista1.0/libmachista.c @@ -52,6 +52,8 @@ #include #ifdef __MACH__ +#include + #include #include #include @@ -230,12 +232,19 @@ static macho_loadcmd_t *macho_loadcmdlist_append (macho_arch_t *mat) { /* Parse a Mach-O header */ #ifdef __MACH__ +static int parse_ar (macho_t *mt, macho_input_t *input); + static int parse_macho (macho_t *mt, macho_input_t *input) { /* Read the file type. */ const uint32_t *magic = macho_read(input, input->data, sizeof(uint32_t)); if (magic == NULL) return MACHO_ERANGE; + /* A static archive (e.g. *.a) is a container of Mach-O objects */ + if (input->length >= SARMAG && memcmp(input->data, ARMAG, SARMAG) == 0) { + return parse_ar(mt, input); + } + /* Parse the Mach-O header */ bool universal = false; uint32_t (*swap32)(uint32_t) = macho_nswap32; @@ -430,6 +439,79 @@ static int parse_macho (macho_t *mt, macho_input_t *input) { } #endif +/* Parse a static archive, collecting the architectures of its Mach-O members */ +#ifdef __MACH__ +static int parse_ar (macho_t *mt, macho_input_t *input) { + /* Skip over the "!\n" magic */ + size_t offset = SARMAG; + + while (offset + sizeof(struct ar_hdr) <= input->length) { + const struct ar_hdr *hdr = macho_offset(input, input->data, offset, sizeof(struct ar_hdr)); + if (hdr == NULL) + return MACHO_ERANGE; + + if (memcmp(hdr->ar_fmag, ARFMAG, sizeof(hdr->ar_fmag)) != 0) + return MACHO_EMAGIC; + + /* ar_size counts the extended name (if any) plus the member data */ + char sizebuf[sizeof(hdr->ar_size) + 1]; + memcpy(sizebuf, hdr->ar_size, sizeof(hdr->ar_size)); + sizebuf[sizeof(hdr->ar_size)] = '\0'; + long member_size = strtol(sizebuf, NULL, 10); + if (member_size <= 0) + return MACHO_ERANGE; + + size_t data_offset = offset + sizeof(struct ar_hdr); + size_t data_size = (size_t) member_size; + + /* BSD ar stores a long name as "#1/" followed by the name */ + size_t name_len = 0; + if (strncmp(hdr->ar_name, AR_EFMT1, sizeof(AR_EFMT1) - 1) == 0) { + name_len = (size_t) strtol(hdr->ar_name + sizeof(AR_EFMT1) - 1, NULL, 10); + if (name_len > data_size) + return MACHO_ERANGE; + } + + macho_input_t member; + member.length = data_size - name_len; + member.data = macho_offset(input, input->data, data_offset + name_len, member.length); + if (member.data == NULL) + return MACHO_ERANGE; + + /* non-Mach-O members (e.g. the symbol table) report EMAGIC; ignore them */ + int res = parse_macho(mt, &member); + if (res != MACHO_SUCCESS && res != MACHO_EMAGIC) + return res; + + /* members are padded to a two-byte boundary */ + offset = data_offset + data_size; + if ((offset & 1) != 0) + offset++; + } + + /* collapse the duplicate archs of the many same-arch objects in an archive */ + for (macho_arch_t *a = mt->mt_archs; a != NULL; a = a->next) { + macho_arch_t *prev = a; + macho_arch_t *cur = a->next; + while (cur != NULL) { + if (cur->mat_arch == a->mat_arch) { + prev->next = cur->next; + free_macho_arch_t(cur); + cur = prev->next; + } else { + prev = cur; + cur = cur->next; + } + } + } + + if (mt->mt_archs == NULL) + return MACHO_EMAGIC; + + return MACHO_SUCCESS; +} +#endif + /* Parse a (possible Mach-O) file. For a more detailed description, see the header */ #ifdef __MACH__ int macho_parse_file(macho_handle_t *handle, const char *filepath, const macho_t **res) { diff --git a/src/machista1.0/tests/libmachista-test.c b/src/machista1.0/tests/libmachista-test.c index 6dab13c34..0351e1517 100644 --- a/src/machista1.0/tests/libmachista-test.c +++ b/src/machista1.0/tests/libmachista-test.c @@ -11,6 +11,7 @@ #include #define TEST_LIB_PATH "tests/libmachista-test-lib.dylib" +#define TEST_ARCHIVE_PATH "tests/libmachista-test-archive.a" #define OTOOL_PATH "/usr/bin/otool" // check helper @@ -305,6 +306,47 @@ static bool test_libsystem(void) { return false; } +/** + * Test reading a static archive (e.g. a *.a library). The fixture holds two + * objects of the same arch, so parsing must succeed and report exactly one + * (deduplicated), valid architecture. + */ +static void forked_test_archive(void) { + macho_handle_t *handle = macho_create_handle(); + const macho_t *result; + int ret; + bool ok = true; + + if ((ret = macho_parse_file(handle, TEST_ARCHIVE_PATH, &result)) != MACHO_SUCCESS) { + printf("\tError parsing `%s': %s\n", TEST_ARCHIVE_PATH, macho_strerror(ret)); + macho_destroy_handle(handle); + exit(EXIT_FAILURE); + } + + if (result->mt_archs == NULL) { + printf("\tNo architecture reported for `%s'\n", TEST_ARCHIVE_PATH); + ok = false; + } else if (result->mt_archs->next != NULL) { + printf("\tExpected a single (deduplicated) architecture for `%s'\n", TEST_ARCHIVE_PATH); + ok = false; + } else if (macho_get_arch_name(result->mt_archs->mat_arch) == NULL) { + printf("\tUnknown architecture reported for `%s'\n", TEST_ARCHIVE_PATH); + ok = false; + } + + macho_destroy_handle(handle); + exit(!ok); +} +static bool test_archive(void) { + puts("Testing parsing " TEST_ARCHIVE_PATH); + if (fork_test(forked_test_archive, "Error parsing " TEST_ARCHIVE_PATH)) { + puts("\tOK"); + return true; + } + puts("\tError"); + return false; +} + /** * Test macho_format_dylib_version */ @@ -351,6 +393,7 @@ int main(void) { result &= test_handle(); result &= test_format_dylib_version(); result &= test_libsystem(); + result &= test_archive(); return !result; #else return 0; diff --git a/src/pextlib1.0/Pextlib.c b/src/pextlib1.0/Pextlib.c index 989d8ad17..f68c4a033 100644 --- a/src/pextlib1.0/Pextlib.c +++ b/src/pextlib1.0/Pextlib.c @@ -80,6 +80,7 @@ #endif #ifdef __MACH__ +#include #include #include #endif @@ -559,6 +560,16 @@ static int fileIsBinaryCmd(ClientData clientData UNUSED, Tcl_Interp *interp, int Tcl_SetObjResult(interp, Tcl_NewBooleanObj(true)); return TCL_OK; } + if (memcmp(&magic, ARMAG, sizeof(magic)) == 0) { + /* static archive ("!\n", e.g. *.a); holds mach-o objects */ + char rest[SARMAG - sizeof(magic)]; + if (sizeof(rest) == fread(rest, 1, sizeof(rest), file) + && memcmp(rest, &ARMAG[sizeof(magic)], sizeof(rest)) == 0) { + fclose(file); + Tcl_SetObjResult(interp, Tcl_NewBooleanObj(true)); + return TCL_OK; + } + } if (magic == htonl(FAT_MAGIC)) { uint32_t archcount; /* either universal binary or java class (FAT_MAGIC == 0xcafebabe) diff --git a/src/port1.0/porttest.tcl b/src/port1.0/porttest.tcl index 96ddbe441..62c5af69f 100644 --- a/src/port1.0/porttest.tcl +++ b/src/port1.0/porttest.tcl @@ -33,7 +33,7 @@ proc porttest::get_file_archs {handle fpath} { set returncode [lindex $resultlist 0] set result [lindex $resultlist 1] if {$returncode != $::machista::SUCCESS} { - # fails on static libs, ignore + # EMAGIC means not Mach-O (nor an archive of Mach-O objects); ignore if {$returncode != $::machista::EMAGIC} { ui_warn "Error parsing file ${fpath}: [machista::strerror $returncode]" }