diff options
| author | Joseph Hunkeler <jhunkeler@users.noreply.github.com> | 2026-04-07 15:39:47 -0400 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-07 15:39:47 -0400 |
| commit | 5b5abcce09da3ce1cc8fab57e1571d0ff0966f7b (patch) | |
| tree | 66e6e8eb470bcae56885d2ca15b67bfdd0b369f3 | |
| parent | d01b465eee667e8efa4aa7c3088dc7af18ea2ab2 (diff) | |
| parent | 666e1f06d6be94114f4db5b2a4cb75d5e1ecb445 (diff) | |
| download | stasis-5b5abcce09da3ce1cc8fab57e1571d0ff0966f7b.tar.gz | |
Merge pull request #123 from jhunkeler/wheel-parser
Implement Python wheel interface
| -rw-r--r-- | .github/workflows/cmake-multi-platform.yml | 9 | ||||
| -rw-r--r-- | CMakeLists.txt | 10 | ||||
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | src/lib/core/CMakeLists.txt | 4 | ||||
| -rw-r--r-- | src/lib/core/include/strlist.h | 1 | ||||
| -rw-r--r-- | src/lib/core/include/wheel.h | 277 | ||||
| -rw-r--r-- | src/lib/core/include/wheelinfo.h | 36 | ||||
| -rw-r--r-- | src/lib/core/strlist.c | 23 | ||||
| -rw-r--r-- | src/lib/core/wheel.c | 1433 | ||||
| -rw-r--r-- | src/lib/core/wheelinfo.c | 129 | ||||
| -rw-r--r-- | src/lib/delivery/delivery_init.c | 2 | ||||
| -rw-r--r-- | src/lib/delivery/delivery_install.c | 6 | ||||
| -rw-r--r-- | src/lib/delivery/include/delivery.h | 2 | ||||
| -rw-r--r-- | tests/test_wheel.c | 275 | ||||
| -rw-r--r-- | tests/test_wheelinfo.c | 91 |
15 files changed, 2087 insertions, 212 deletions
diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 8586e19..85255c4 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -49,9 +49,16 @@ jobs: libcurl4-openssl-dev libxml2-dev libxml2-utils + libzip-dev pandoc rsync + - name: Install macOS dependencies + if: matrix.os == 'macos-latest' + run: > + brew install + libzip + - name: Configure CMake run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} @@ -64,6 +71,8 @@ jobs: -S ${{ github.workspace }} - name: Build + env: + PKG_CONFIG_PATH: /opt/homebrew/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} - name: Test diff --git a/CMakeLists.txt b/CMakeLists.txt index 074c2ee..3a5576f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,9 +14,17 @@ if (ASAN) add_link_options(-fsanitize=address) endif() +pkg_check_modules(ZIP libzip) +if (NOT ZIP_FOUND) + message(FATAL_ERROR "libzip is required (https://libzip.org)") +endif () + +include_directories(${ZIP_INCLUDEDIR}) +link_directories(${ZIP_LIBRARY_DIRS}) +include_directories(${CURL_INCLUDE_DIR}) link_libraries(CURL::libcurl) -link_libraries(LibXml2::LibXml2) include_directories(${LIBXML2_INCLUDE_DIR}) +link_libraries(LibXml2::LibXml2) option(FORTIFY_SOURCE OFF) if (FORTIFY_SOURCE) @@ -8,6 +8,7 @@ STASIS consolidates the steps required to build, test, and deploy calibration pi - cmake - libcurl - libxml2 +- libzip - rsync # Installation diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt index eb7a908..0fb273c 100644 --- a/src/lib/core/CMakeLists.txt +++ b/src/lib/core/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(stasis_core STATIC download.c recipe.c relocation.c + wheelinfo.c wheel.c copy.c artifactory.c @@ -27,5 +28,8 @@ add_library(stasis_core STATIC target_include_directories(stasis_core PRIVATE ${core_INCLUDE} ${delivery_INCLUDE} + ${ZIP_INCLUDEDIR} ${CMAKE_CURRENT_SOURCE_DIR}/include ) +target_link_libraries(stasis_core PRIVATE + ${ZIP_LIBRARIES}) diff --git a/src/lib/core/include/strlist.h b/src/lib/core/include/strlist.h index 1aaae3e..b2d7da7 100644 --- a/src/lib/core/include/strlist.h +++ b/src/lib/core/include/strlist.h @@ -46,6 +46,7 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2 void strlist_append(struct StrList **pStrList, char *str); void strlist_append_array(struct StrList *pStrList, char **arr); void strlist_append_tokenize(struct StrList *pStrList, char *str, char *delim); +void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim); int strlist_appendf(struct StrList **pStrList, const char *fmt, ...); struct StrList *strlist_copy(struct StrList *pStrList); int strlist_cmp(struct StrList *a, struct StrList *b); diff --git a/src/lib/core/include/wheel.h b/src/lib/core/include/wheel.h index 1a689e9..765f2c3 100644 --- a/src/lib/core/include/wheel.h +++ b/src/lib/core/include/wheel.h @@ -1,36 +1,257 @@ -//! @file wheel.h -#ifndef STASIS_WHEEL_H -#define STASIS_WHEEL_H +#ifndef WHEEL_H +#define WHEEL_H -#include <dirent.h> -#include <string.h> #include <stdio.h> -#include "str.h" -#define WHEEL_MATCH_EXACT 0 ///< Match when all patterns are present -#define WHEEL_MATCH_ANY 1 ///< Match when any patterns are present +#include <stdlib.h> +#include <string.h> +#include <fnmatch.h> +#include <zip.h> + +#define WHEEL_MAXELEM 255 + +#define WHEEL_FROM_DIST 0 +#define WHEEL_FROM_METADATA 1 + +enum { + WHEEL_META_METADATA_VERSION=0, + WHEEL_META_NAME, + WHEEL_META_VERSION, + WHEEL_META_AUTHOR, + WHEEL_META_AUTHOR_EMAIL, + WHEEL_META_MAINTAINER, + WHEEL_META_MAINTAINER_EMAIL, + WHEEL_META_SUMMARY, + WHEEL_META_LICENSE, + WHEEL_META_LICENSE_EXPRESSION, + WHEEL_META_LICENSE_FILE, + WHEEL_META_HOME_PAGE, + WHEEL_META_DOWNLOAD_URL, + WHEEL_META_PROJECT_URL, + WHEEL_META_CLASSIFIER, + WHEEL_META_REQUIRES_PYTHON, + WHEEL_META_REQUIRES_EXTERNAL, + WHEEL_META_IMPORT_NAME, + WHEEL_META_IMPORT_NAMESPACE, + WHEEL_META_REQUIRES_DIST, + WHEEL_META_PROVIDES, + WHEEL_META_PROVIDES_DIST, + WHEEL_META_PROVIDES_EXTRA, + WHEEL_META_OBSOLETES, + WHEEL_META_OBSOLETES_DIST, + WHEEL_META_PLATFORM, + WHEEL_META_SUPPORTED_PLATFORM, + WHEEL_META_KEYWORDS, + WHEEL_META_DYNAMIC, + WHEEL_META_DESCRIPTION_CONTENT_TYPE, + WHEEL_META_DESCRIPTION, + WHEEL_META_END_ENUM, // NOP +}; + + +enum { + WHEEL_DIST_VERSION=0, + WHEEL_DIST_GENERATOR, + WHEEL_DIST_ROOT_IS_PURELIB, + WHEEL_DIST_TAG, + WHEEL_DIST_ZIP_SAFE, + WHEEL_DIST_TOP_LEVEL, + WHEEL_DIST_ENTRY_POINT, + WHEEL_DIST_RECORD, + WHEEL_DIST_END_ENUM, // NOP +}; + +struct WheelMetadata_ProvidesExtra { + char *target; + struct StrList *requires_dist; + int count; +}; + +struct WheelMetadata { + char *metadata_version; + char *name; + char *version; + char *summary; + struct StrList *author; + struct StrList *author_email; + struct StrList *maintainer; + struct StrList *maintainer_email; + char *license; + char *license_expression; + char *home_page; + char * download_url; + struct StrList *project_url; + struct StrList *classifier; + struct StrList *requires_python; + struct StrList *requires_external; + char *description_content_type; + struct StrList *license_file; + struct StrList *import_name; + struct StrList *import_namespace; + struct StrList *requires_dist; + struct StrList *provides; + struct StrList *provides_dist; + struct StrList *obsoletes; + struct StrList *obsoletes_dist; + char *description; + struct StrList *platform; + struct StrList *supported_platform; + struct StrList *keywords; + struct StrList *dynamic; + + struct WheelMetadata_ProvidesExtra **provides_extra; +}; + +struct WheelRecord { + char *filename; + char *checksum; + size_t size; +}; + +struct WheelEntryPoint { + char *name; + char *function; + char *type; +}; + +/* +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: false +Tag: cp313-cp313-manylinux_2_17_x86_64 +Tag: cp313-cp313-manylinux2014_x86_64 +*/ struct Wheel { - char *distribution; ///< Package name - char *version; ///< Package version - char *build_tag; ///< Package build tag (optional) - char *python_tag; ///< Package Python tag (pyXY) - char *abi_tag; ///< Package ABI tag (cpXY, abiX, none) - char *platform_tag; ///< Package platform tag (linux_x86_64, any) - char *path_name; ///< Path to package on-disk - char *file_name; ///< Name of package on-disk + char *wheel_version; + char *generator; + char *root_is_pure_lib; + struct StrList *tag; + struct StrList *top_level; + int zip_safe; + struct WheelMetadata *metadata; + struct WheelRecord **record; + size_t num_record; + struct WheelEntryPoint **entry_point; + size_t num_entry_point; +}; + +#define METADATA_MULTILINE_PREFIX " " + + +static inline int consume_append(char **dest, const char *src, const char *accept) { + const char *start = src; + if (!strncmp(src, METADATA_MULTILINE_PREFIX, strlen(METADATA_MULTILINE_PREFIX))) { + start += strlen(METADATA_MULTILINE_PREFIX); + } + + const char *end = strpbrk(start, accept); + size_t cut_len = end ? (size_t)(end - start) : strlen(start); + size_t dest_len = strlen(*dest); + + char *tmp = realloc(*dest, strlen(*dest) + cut_len + 2); + if (!tmp) { + return -1; + } + *dest = tmp; + memcpy(*dest + dest_len, start, cut_len); + + dest_len += cut_len; + (*dest)[dest_len ? dest_len : 0] = '\n'; + (*dest)[dest_len + 1] = '\0'; + return 0; +} + +#define WHEEL_KEY_UNKNOWN (-1) +enum { + WHEELVAL_STR = 0, + WHEELVAL_STRLIST, + WHEELVAL_OBJ_EXTRA, + WHEELVAL_OBJ_RECORD, + WHEELVAL_OBJ_ENTRY_POINT, +}; + +struct WheelValue { + int type; + size_t count; + void *data; }; +enum { + WHEEL_PACKAGE_E_SUCCESS=0, + WHEEL_PACKAGE_E_FILENAME=-1, + WHEEL_PACKAGE_E_ALLOC=-2, + WHEEL_PACKAGE_E_GET=-3, + WHEEL_PACKAGE_E_GET_METADATA=-4, + WHEEL_PACKAGE_E_GET_TOP_LEVEL=-5, + WHEEL_PACKAGE_E_GET_RECORDS=-6, + WHEEL_PACKAGE_E_GET_ENTRY_POINT=-7, +}; + +/** + * Populate a `Wheel` structure using a Python wheel file as input. + * + * @param pkg pointer to a `Wheel` (may be initialized to `NULL`) + * @param filename path to a Python wheel file + * @return a WHEEL_PACKAGE_E_ error code + */ +int wheel_package(struct Wheel **pkg, const char *filename); + +/** + * Frees a `Wheel` structure + * @param pkg pointer to an initialized `Wheel` + */ +void wheel_package_free(struct Wheel **pkg); + + +/** + * Get wheel data by name + * @param pkg pointer to an initialized `Wheel` + * @param from `WHEEL_FROM_DIST`, `WHEEL_FROM_META` + * @param key name of key in DIST or META data + * @return a populated `WheelValue` (stack) + */ +struct WheelValue wheel_get_value_by_name(const struct Wheel *pkg, int from, const char *key); + + /** - * Extract metadata from a Python Wheel file name + * Get wheel data by internal identifier + * @param pkg pointer to an initialized `Wheel` + * @param from `WHEEL_FROM_DIST`, `WHEEL_FROM_META` + * @param id `WHEEL_META_VERSION`, `WHEEL_DIST_VERSION` (see wheel.h) + * @return a populated `WheelValue` (stack) + */ +struct WheelValue wheel_get_value_by_id(const struct Wheel *pkg, int from, ssize_t id); + +/** + * Returns the error code assocated with the `WheelValue`, if possible + * @param val a populated `WheelValue` + * @return error code (see wheel.h) + */ +int wheel_value_error(struct WheelValue const *val); + +/** + * Retreive the key name string for a given id + * @param from `WHEEL_FROM_DIST`, `WHEEL_FROM_META` + * @param id `WHEEL_META_VERSION`, `WHEEL_DIST_VERSION` (see wheel.h) + * @return the key name, or NULL + */ +const char *wheel_get_key_by_id(int from, ssize_t id); + +/** + * Get the contents of a file within a Python wheel + * @param wheelfile path to Python wheel file + * @param filename path to file inside of wheel file archive + * @param contents pointer to store file contents + * @return 0 on success, -1 on error + */ +int wheel_get_file_contents(const char *wheelfile, const char *filename, char **contents); + +/** + * Display the values of a `Wheel` structure in human readable format * - * @param basepath directory containing a wheel file - * @param name of wheel file - * @param to_match a NULL terminated array of patterns (i.e. platform, arch, version, etc) - * @param match_mode WHEEL_MATCH_EXACT - * @param match_mode WHEEL_MATCH ANY - * @return pointer to populated Wheel on success - * @return NULL on error - */ -struct Wheel *get_wheel_info(const char *basepath, const char *name, char *to_match[], unsigned match_mode); -void wheel_free(struct Wheel **wheel); -#endif //STASIS_WHEEL_H + * @param wheel + * @return 0 on success, -1 on error + */ +int wheel_show_info(const struct Wheel *wheel); + +#endif //WHEEL_H diff --git a/src/lib/core/include/wheelinfo.h b/src/lib/core/include/wheelinfo.h new file mode 100644 index 0000000..8009e91 --- /dev/null +++ b/src/lib/core/include/wheelinfo.h @@ -0,0 +1,36 @@ +//! @file wheel.h +#ifndef STASIS_WHEEL_H +#define STASIS_WHEEL_H + +#include <dirent.h> +#include <string.h> +#include <stdio.h> +#include "str.h" +#define WHEEL_MATCH_EXACT 0 ///< Match when all patterns are present +#define WHEEL_MATCH_ANY 1 ///< Match when any patterns are present + +struct WheelInfo { + char *distribution; ///< Package name + char *version; ///< Package version + char *build_tag; ///< Package build tag (optional) + char *python_tag; ///< Package Python tag (pyXY) + char *abi_tag; ///< Package ABI tag (cpXY, abiX, none) + char *platform_tag; ///< Package platform tag (linux_x86_64, any) + char *path_name; ///< Path to package on-disk + char *file_name; ///< Name of package on-disk +}; + +/** + * Extract metadata from a Python Wheel file name + * + * @param basepath directory containing a wheel file + * @param name of wheel file + * @param to_match a NULL terminated array of patterns (i.e. platform, arch, version, etc) + * @param match_mode WHEEL_MATCH_EXACT + * @param match_mode WHEEL_MATCH ANY + * @return pointer to populated Wheel on success + * @return NULL on error + */ +struct WheelInfo *wheelinfo_get(const char *basepath, const char *name, char *to_match[], unsigned match_mode); +void wheelinfo_free(struct WheelInfo **wheel); +#endif //STASIS_WHEEL_H diff --git a/src/lib/core/strlist.c b/src/lib/core/strlist.c index 3479c44..f3754c3 100644 --- a/src/lib/core/strlist.c +++ b/src/lib/core/strlist.c @@ -230,6 +230,29 @@ void strlist_append_strlist(struct StrList *pStrList1, struct StrList *pStrList2 } /** + * Append the contents of a newline delimited string without + * modifying the input `str` + * @param pStrList `StrList` + * @param str + * @param delim + */ +void strlist_append_tokenize_raw(struct StrList *pStrList, char *str, char *delim) { + if (!str || !delim) { + return; + } + + char *tmp = strdup(str); + char **token = split(tmp, delim, 0); + if (token) { + for (size_t i = 0; token[i] != NULL; i++) { + strlist_append(&pStrList, token[i]); + } + guard_array_free(token); + } + guard_free(tmp); +} + +/** * Append a formatted string * Behavior is identical to asprintf-family of functions * @param pStrList `StrList` diff --git a/src/lib/core/wheel.c b/src/lib/core/wheel.c index 79b5a21..78209f1 100644 --- a/src/lib/core/wheel.c +++ b/src/lib/core/wheel.c @@ -1,129 +1,1356 @@ #include "wheel.h" -struct Wheel *get_wheel_info(const char *basepath, const char *name, char *to_match[], unsigned match_mode) { - struct dirent *rec; - struct Wheel *result = NULL; - char package_path[PATH_MAX]; - char package_name[NAME_MAX]; +#include <ctype.h> - strcpy(package_name, name); - tolower_s(package_name); - sprintf(package_path, "%s/%s", basepath, package_name); +#include "str.h" +#include "strlist.h" - DIR *dp = opendir(package_path); - if (!dp) { - return NULL; +const char *WHEEL_META_KEY[] = { + "Metadata-Version", + "Name", + "Version", + "Author", + "Author-email", + "Maintainer", + "Maintainer-email", + "Summary", + "License", + "License-Expression", + "License-File", + "Home-page", + "Download-URL", + "Project-URL", + "Classifier", + "Requires-Python", + "Requires-External", + "Import-Name", + "Import-Namespace", + "Requires-Dist", + "Provides", + "Provides-Dist", + "Provides-Extra", + "Obsoletes", + "Obsoletes-Dist", + "Platform", + "Supported-Platform", + "Keywords", + "Dynamic", + "Description-Content-Type", + "Description", + NULL, +}; + +const char *WHEEL_DIST_KEY[] = { + "Wheel-Version", + "Generator", + "Root-Is-Purelib", + "Tag", + "Zip-Safe", + "Top-Level", + "Entry-points", + "Record", + NULL, +}; + +static ssize_t wheel_parse_wheel(struct Wheel * pkg, const char * data) { + int read_as = 0; + struct StrList *lines = strlist_init(); + if (!lines) { + return -1; } + strlist_append_tokenize(lines, (char *) data, "\r\n"); - while ((rec = readdir(dp)) != NULL) { - if (!strcmp(rec->d_name, ".") || !strcmp(rec->d_name, "..")) { + for (size_t i = 0; i < strlist_count(lines); i++) { + char *line = strlist_item(lines, i); + if (isempty(line)) { continue; } - char filename[NAME_MAX]; - strcpy(filename, rec->d_name); - char *ext = strstr(filename, ".whl"); - if (ext) { - *ext = '\0'; + + char **pair = split(line, ":", 1); + if (pair) { + char *key = strdup(strip(pair[0])); + char *value = strdup(lstrip(pair[1])); + + if (!key || !value) { + return -1; + } + + if (!strcasecmp(key, WHEEL_DIST_KEY[WHEEL_DIST_VERSION])) { + read_as = WHEEL_DIST_VERSION; + } else if (!strcasecmp(key, WHEEL_DIST_KEY[WHEEL_DIST_GENERATOR])) { + read_as = WHEEL_DIST_GENERATOR; + } else if (!strcasecmp(key, WHEEL_DIST_KEY[WHEEL_DIST_ROOT_IS_PURELIB])) { + read_as = WHEEL_DIST_ROOT_IS_PURELIB; + } else if (!strcasecmp(key, WHEEL_DIST_KEY[WHEEL_DIST_TAG])) { + read_as = WHEEL_DIST_TAG; + } + + switch (read_as) { + case WHEEL_DIST_VERSION: { + pkg->wheel_version = strdup(value); + if (!pkg->wheel_version) { + // memory error + return -1; + } + break; + } + case WHEEL_DIST_GENERATOR: { + pkg->generator = strdup(value); + if (!pkg->generator) { + // memory error + return -1; + } + break; + } + case WHEEL_DIST_ROOT_IS_PURELIB: { + pkg->root_is_pure_lib = strdup(value); + if (!pkg->root_is_pure_lib) { + // memory error + return -1; + } + break; + } + case WHEEL_DIST_TAG: { + if (!pkg->tag) { + pkg->tag = strlist_init(); + if (!pkg->tag) { + return -1; + } + } + strlist_append(&pkg->tag, value); + break; + } + default: + fprintf(stderr, "warning: unhandled wheel key on line %zu:\nbuffer contents: '%s'\n", i, value); + break; + } + guard_free(key); + guard_free(value); + } + guard_array_free(pair); + } + guard_strlist_free(&lines); + return data ? (ssize_t) strlen(data) : -1; +} + + +static inline int is_continuation(const char *s) { + return s && (s[0] == ' ' || s[0] == '\t'); +} + +static ssize_t wheel_parse_metadata(struct WheelMetadata * const pkg, const char * const data) { + int read_as = WHEEL_KEY_UNKNOWN; + // triggers + int reading_multiline = 0; + int reading_extra = 0; + size_t provides_extra_i = 0; + int reading_description = 0; + size_t base_description_len = 1024; + size_t len_description = 0; + struct WheelMetadata_ProvidesExtra *current_extra = NULL; + + if (!data) { + // data can't be NULL + return -1; + } + + pkg->provides_extra = calloc(WHEEL_MAXELEM + 1, sizeof(pkg->provides_extra[0])); + if (!pkg->provides_extra) { + // memory error + return -1; + } + + struct StrList *lines = strlist_init(); + if (!lines) { + // memory error + return -1; + } + + strlist_append_tokenize_raw(lines, (char *) data, "\r\n"); + for (size_t i = 0; i < strlist_count(lines); i++) { + const char *line = strlist_item(lines, i); + char *key = NULL; + char *value = NULL; + + reading_multiline = is_continuation(line); + if (!reading_multiline && line[0] == '\0') { + reading_description = 1; + read_as = WHEEL_META_DESCRIPTION; + } + + char **pair = split((char *) line, ":", 1); + if (!pair) { + // memory error + return -1; + } + + if (!reading_description && !reading_multiline && pair[1]) { + key = strip(strdup(pair[0])); + value = lstrip(strdup(pair[1])); + + if (!key || !value) { + // memory error + return -1; + } + + if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_METADATA_VERSION])) { + read_as = WHEEL_META_METADATA_VERSION; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_NAME])) { + read_as = WHEEL_META_NAME; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_VERSION])) { + read_as = WHEEL_META_VERSION; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_SUMMARY])) { + read_as = WHEEL_META_SUMMARY; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_AUTHOR])) { + read_as = WHEEL_META_AUTHOR; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_AUTHOR_EMAIL])) { + read_as = WHEEL_META_AUTHOR_EMAIL; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_MAINTAINER])) { + read_as = WHEEL_META_MAINTAINER; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_MAINTAINER_EMAIL])) { + read_as = WHEEL_META_MAINTAINER_EMAIL; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_LICENSE])) { + read_as = WHEEL_META_LICENSE; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_LICENSE_EXPRESSION])) { + read_as = WHEEL_META_LICENSE_EXPRESSION; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_HOME_PAGE])) { + read_as = WHEEL_META_HOME_PAGE; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_DOWNLOAD_URL])) { + read_as = WHEEL_META_DOWNLOAD_URL; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_PROJECT_URL])) { + read_as = WHEEL_META_PROJECT_URL; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_CLASSIFIER])) { + read_as = WHEEL_META_CLASSIFIER; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_REQUIRES_PYTHON])) { + read_as = WHEEL_META_REQUIRES_PYTHON; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_REQUIRES_EXTERNAL])) { + read_as = WHEEL_META_REQUIRES_EXTERNAL; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_DESCRIPTION_CONTENT_TYPE])) { + read_as = WHEEL_META_DESCRIPTION_CONTENT_TYPE; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_LICENSE_FILE])) { + read_as = WHEEL_META_LICENSE_FILE; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_REQUIRES_DIST])) { + read_as = WHEEL_META_REQUIRES_DIST; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_PROVIDES])) { + read_as = WHEEL_META_PROVIDES; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_IMPORT_NAME])) { + read_as = WHEEL_META_IMPORT_NAME; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_IMPORT_NAMESPACE])) { + read_as = WHEEL_META_IMPORT_NAMESPACE; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_PROVIDES_DIST])) { + read_as = WHEEL_META_PROVIDES_DIST; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_PROVIDES_EXTRA])) { + read_as = WHEEL_META_PROVIDES_EXTRA; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_PLATFORM])) { + read_as = WHEEL_META_PLATFORM; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_SUPPORTED_PLATFORM])) { + read_as = WHEEL_META_SUPPORTED_PLATFORM; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_KEYWORDS])) { + read_as = WHEEL_META_KEYWORDS; + } else if (!strcasecmp(key,WHEEL_META_KEY[WHEEL_META_DYNAMIC])) { + read_as = WHEEL_META_DYNAMIC; + } else { + read_as = WHEEL_KEY_UNKNOWN; + } } else { - // not a wheel file. nothing to do - continue; + value = strdup(line); + if (!value) { + // memory error + return -1; + } } - size_t match = 0; - size_t pattern_count = 0; - for (; to_match[pattern_count] != NULL; pattern_count++) { - if (strstr(filename, to_match[pattern_count])) { - match++; + switch (read_as) { + case WHEEL_META_METADATA_VERSION: { + pkg->metadata_version = strdup(value); + if (!pkg->metadata_version) { + // memory error + return -1; + } + break; + } + case WHEEL_META_NAME: { + pkg->name = strdup(value); + if (!pkg->name) { + // memory error + return -1; + } + break; + } + case WHEEL_META_VERSION: { + pkg->version = strdup(value); + if (!pkg->version) { + // memory error + return -1; + } + break; + } + case WHEEL_META_SUMMARY: { + pkg->summary = strdup(value); + if (!pkg->summary) { + // memory error + return -1; + } + break; + } + case WHEEL_META_AUTHOR: { + if (!pkg->author) { + pkg->author = strlist_init(); + if (!pkg->author) { + // memory error + return -1; + } + } + strlist_append_tokenize(pkg->author, value, ","); + break; + } + case WHEEL_META_AUTHOR_EMAIL: { + if (!pkg->author_email) { + pkg->author_email = strlist_init(); + if (!pkg->author_email) { + // memory error + return -1; + } + } + strlist_append_tokenize(pkg->author_email, value, ","); + break; + } + case WHEEL_META_MAINTAINER: { + if (!pkg->maintainer) { + pkg->maintainer = strlist_init(); + if (!pkg->maintainer) { + // memory error + return -1; + } + } + strlist_append_tokenize(pkg->maintainer, value, ","); + break; + } + case WHEEL_META_MAINTAINER_EMAIL: { + if (!pkg->maintainer_email) { + pkg->maintainer_email = strlist_init(); + if (!pkg->maintainer_email) { + // memory error + return -1; + } + } + strlist_append_tokenize(pkg->maintainer_email, value, ","); + break; + } + case WHEEL_META_LICENSE: { + if (!reading_multiline) { + pkg->license = strdup(value); + if (!pkg->license) { + // memory error + return -1; + } + } else { + if (pkg->license) { + consume_append(&pkg->license, line, "\r\n"); + } else { + // previously unhandled memory error + return -1; + } + } + break; + } + case WHEEL_META_LICENSE_EXPRESSION: { + pkg->license_expression = strdup(value); + if (!pkg->license_expression) { + // memory error + return -1; + } + break; + } + case WHEEL_META_HOME_PAGE: { + pkg->home_page = strdup(value); + if (!pkg->home_page) { + // memory error + return -1; + } + break; + } + case WHEEL_META_DOWNLOAD_URL: + pkg->download_url = strdup(value); + if (!pkg->download_url) { + // memory error + return -1; + } + break; + case WHEEL_META_PROJECT_URL: { + if (!pkg->project_url) { + pkg->project_url = strlist_init(); + if (!pkg->project_url) { + // memory_error + return -1; + } + } + strlist_append(&pkg->project_url, value); + break; + } + case WHEEL_META_CLASSIFIER: { + if (!pkg->classifier) { + pkg->classifier = strlist_init(); + if (!pkg->classifier) { + // memory error + return -1; + } + } + strlist_append(&pkg->classifier, value); + break; + } + case WHEEL_META_REQUIRES_PYTHON: { + if (!pkg->requires_python) { + pkg->requires_python = strlist_init(); + if (!pkg->requires_python) { + // memory error + return -1; + } + } + strlist_append(&pkg->requires_python, value); + break; + } + case WHEEL_META_REQUIRES_EXTERNAL: { + if (!pkg->requires_external) { + pkg->requires_external = strlist_init(); + if (!pkg->requires_external) { + // memory error + return -1; + } + } + strlist_append(&pkg->requires_external, value); + break; + } + case WHEEL_META_DESCRIPTION_CONTENT_TYPE: { + pkg->description_content_type = strdup(value); + if (!pkg->description_content_type) { + // memory error + return -1; + } + break; + } + case WHEEL_META_LICENSE_FILE: { + if (!pkg->license_file) { + pkg->license_file = strlist_init(); + if (!pkg->license_file) { + // memory error + return -1; + } + } + strlist_append(&pkg->license_file, value); + break; + } + case WHEEL_META_IMPORT_NAME: { + if (!pkg->import_name) { + pkg->import_name = strlist_init(); + if (!pkg->import_name) { + // memory error + return -1; + } + } + strlist_append(&pkg->import_name, value); + break; + } + case WHEEL_META_IMPORT_NAMESPACE: { + if (!pkg->import_namespace) { + pkg->import_namespace = strlist_init(); + if (!pkg->import_namespace) { + // memory error + return -1; + } + } + strlist_append(&pkg->import_namespace, value); + break; + } + case WHEEL_META_REQUIRES_DIST: { + if (!pkg->requires_dist) { + pkg->requires_dist = strlist_init(); + if (!pkg->requires_dist) { + // memory error + return -1; + } + } + if (reading_extra) { + if (strrchr(value, ';')) { + *strrchr(value, ';') = 0; + } + if (!current_extra) { + reading_extra = 0; + break; + } + if (!current_extra->requires_dist) { + current_extra->requires_dist = strlist_init(); + if (!current_extra->requires_dist) { + // memory error + return -1; + } + } + strlist_append(¤t_extra->requires_dist, value); + } else { + strlist_append(&pkg->requires_dist, value); + reading_extra = 0; + } + break; + } + case WHEEL_META_PROVIDES: { + if (!pkg->provides) { + pkg->provides = strlist_init(); + if (!pkg->provides) { + // memory error + return -1; + } + } + strlist_append(&pkg->provides, value); + break; + } + case WHEEL_META_PROVIDES_DIST: { + if (!pkg->provides_dist) { + pkg->provides_dist = strlist_init(); + if (!pkg->provides_dist) { + // memory error + return -1; + } + } + strlist_append(&pkg->provides_dist, value); + break; + } + case WHEEL_META_PROVIDES_EXTRA: + pkg->provides_extra[provides_extra_i] = calloc(1, sizeof(*pkg->provides_extra[0])); + if (!pkg->provides_extra[provides_extra_i]) { + // memory error + return -1; + } + current_extra = pkg->provides_extra[provides_extra_i]; + current_extra->target = strdup(value); + if (!current_extra->target) { + // memory error + return -1; + } + reading_extra = 1; + provides_extra_i++; + break; + case WHEEL_META_PLATFORM:{ + if (!pkg->platform) { + pkg->platform = strlist_init(); + if (!pkg->platform) { + // memory error + return -1; + } + } + strlist_append(&pkg->platform, value); + break; + } + case WHEEL_META_SUPPORTED_PLATFORM: { + if (!pkg->supported_platform) { + pkg->supported_platform = strlist_init(); + if (!pkg->supported_platform) { + // memory error + return -1; + } + } + strlist_append(&pkg->supported_platform, value); + break; } + case WHEEL_META_KEYWORDS: { + if (!pkg->keywords) { + pkg->keywords = strlist_init(); + if (!pkg->keywords) { + // memory error + return -1; + } + } + strlist_append_tokenize(pkg->keywords, value, ","); + break; + } + case WHEEL_META_DYNAMIC: { + if (!pkg->dynamic) { + pkg->dynamic = strlist_init(); + if (!pkg->dynamic) { + // memory error + return -1; + } + } + strlist_append(&pkg->dynamic, value); + break; + } + case WHEEL_META_DESCRIPTION: { + // reading_description will never be reset to zero + reading_description = 1; + if (!pkg->description) { + pkg->description = malloc(base_description_len + 1); + if (!pkg->description) { + return -1; + } + len_description = snprintf(pkg->description, base_description_len, "%s\n", line); + } else { + const size_t next_len = snprintf(NULL, 0, "%s\n%s\n", pkg->description, line); + if (next_len + 1 > base_description_len) { + base_description_len *= 2; + char *tmp = realloc(pkg->description, base_description_len + 1); + if (!tmp) { + // memory error + guard_free(pkg->description); + return -1; + } + pkg->description = tmp; + } + len_description += snprintf(pkg->description + len_description, next_len + 1, "%s\n", line); + + //consume_append(&pkg->description, line, "\r\n"); + } + break; + } + case WHEEL_KEY_UNKNOWN: + default: + fprintf(stderr, "warning: unhandled metadata key on line %zu:\nbuffer contents: '%s'\n", i, value); + break; } + guard_free(key); + guard_free(value); + guard_array_free(pair); + if (reading_multiline) { + guard_free(value); + } + } + guard_strlist_free(&lines); + + return 0; +} + +int wheel_get_file_contents(const char *wheelfile, const char *filename, char **contents) { + int status = 0; + int err = 0; + struct zip_stat archive_info; + zip_t *archive = zip_open(wheelfile, 0, &err); + if (!archive) { + return status; + } - if (!startswith(rec->d_name, name)) { + zip_stat_init(&archive_info); + for (size_t i = 0; zip_stat_index(archive, i, 0, &archive_info) >= 0; i++) { + char internal_path[1024] = {0}; + snprintf(internal_path, sizeof(internal_path), "%s", filename); + const int match = fnmatch(internal_path, archive_info.name, 0); + if (match == FNM_NOMATCH) { continue; } + if (match < 0) { + goto GWM_FAIL; + } - if (match_mode == WHEEL_MATCH_EXACT && match != pattern_count) { - continue; + zip_file_t *handle = zip_fopen_index(archive, i, 0); + if (!handle) { + goto GWM_FAIL; } - result = calloc(1, sizeof(*result)); - if (!result) { - SYSERROR("Unable to allocate %zu bytes for wheel struct", sizeof(*result)); - closedir(dp); - return NULL; - } - - result->path_name = realpath(package_path, NULL); - if (!result->path_name) { - SYSERROR("Unable to resolve absolute path to %s: %s", filename, strerror(errno)); - wheel_free(&result); - closedir(dp); - return NULL; - } - result->file_name = strdup(rec->d_name); - if (!result->file_name) { - SYSERROR("Unable to allocate bytes for %s: %s", rec->d_name, strerror(errno)); - wheel_free(&result); - closedir(dp); - return NULL; - } - - size_t parts_total; - char **parts = split(filename, "-", 0); - if (!parts) { - // This shouldn't happen unless a wheel file is present in the - // directory with a malformed file name, or we've managed to - // exhaust the system's memory - SYSERROR("%s has no '-' separators! (Delete this file and try again)", filename); - wheel_free(&result); - closedir(dp); - return NULL; - } - - for (parts_total = 0; parts[parts_total] != NULL; parts_total++) {} - if (parts_total == 5) { - // no build tag - result->distribution = strdup(parts[0]); - result->version = strdup(parts[1]); - result->build_tag = NULL; - result->python_tag = strdup(parts[2]); - result->abi_tag = strdup(parts[3]); - result->platform_tag = strdup(parts[4]); - } else if (parts_total == 6) { - // has build tag - result->distribution = strdup(parts[0]); - result->version = strdup(parts[1]); - result->build_tag = strdup(parts[2]); - result->python_tag = strdup(parts[3]); - result->abi_tag = strdup(parts[4]); - result->platform_tag = strdup(parts[5]); - } else { - SYSERROR("Unknown wheel name format: %s. Expected 5 or 6 strings " - "separated by '-', but got %zu instead", filename, parts_total); - guard_array_free(parts); - wheel_free(&result); - closedir(dp); - return NULL; - } - guard_array_free(parts); + *contents = calloc(archive_info.size + 1, sizeof(**contents)); + if (!*contents) { + zip_fclose(handle); + goto GWM_FAIL; + } + + if (zip_fread(handle, *contents, archive_info.size) < 0) { + zip_fclose(handle); + guard_free(*contents); + goto GWM_FAIL; + } + zip_fclose(handle); break; } - closedir(dp); + + goto GWM_END; + GWM_FAIL: + status = -1; + + GWM_END: + zip_close(archive); + return status; +} + +static int wheel_metadata_get(const struct Wheel *pkg, const char *wheel_filename) { + char *data = NULL; + if (wheel_get_file_contents(wheel_filename, "*.dist-info/METADATA", &data)) { + return -1; + } + const ssize_t result = wheel_parse_metadata(pkg->metadata, data); + char *data_orig = data; + guard_free(data_orig); + return (int) result; +} + +static struct WheelValue wheel_data_dump(const struct Wheel *pkg, const ssize_t key) { + struct WheelValue result; + result.type = WHEELVAL_STR; + result.count = 0; + switch (key) { + case WHEEL_DIST_VERSION: + result.data = pkg->wheel_version; + break; + case WHEEL_DIST_GENERATOR: + result.data = pkg->generator; + break; + case WHEEL_DIST_ZIP_SAFE: + result.data = pkg->zip_safe ? "True" : "False"; + break; + case WHEEL_DIST_ROOT_IS_PURELIB: + result.data = pkg->root_is_pure_lib ? "True" : "False"; + break; + case WHEEL_DIST_TOP_LEVEL: + result.type = WHEELVAL_STRLIST; + result.data = pkg->top_level; + break; + case WHEEL_DIST_TAG: + result.type = WHEELVAL_STRLIST; + result.data = pkg->tag; + break; + case WHEEL_DIST_RECORD: + result.type = WHEELVAL_OBJ_RECORD; + result.data = pkg->record; + result.count = pkg->num_record; + break; + case WHEEL_DIST_ENTRY_POINT: + result.type = WHEELVAL_OBJ_ENTRY_POINT; + result.data = pkg->entry_point; + result.count = pkg->num_entry_point; + break; + default: + result.data = NULL; + result.type = WHEEL_KEY_UNKNOWN; + break; + } + + switch (result.type) { + case WHEELVAL_STR: + result.count = result.data != NULL ? strlen(result.data) : 0; + break; + case WHEELVAL_STRLIST: + result.count = result.data != NULL ? strlist_count(result.data) : 0; + break; + default: + break; + } + + return result; +} + +static struct WheelValue wheel_metadata_dump(const struct Wheel *pkg, const ssize_t key) { + const struct WheelMetadata *meta = pkg->metadata; + struct WheelValue result; + result.type = WHEELVAL_STR; + result.count = 0; + switch (key) { + case WHEEL_META_METADATA_VERSION: + result.data = meta->metadata_version; + break; + case WHEEL_META_NAME: + result.data = meta->name; + break; + case WHEEL_META_VERSION: + result.data = meta->version; + break; + case WHEEL_META_SUMMARY: + result.data = meta->summary; + break; + case WHEEL_META_AUTHOR: + result.type = WHEELVAL_STRLIST; + result.data = meta->author; + break; + case WHEEL_META_AUTHOR_EMAIL: + result.type = WHEELVAL_STRLIST; + result.data = meta->author_email; + break; + case WHEEL_META_MAINTAINER: + result.type = WHEELVAL_STRLIST; + result.data = meta->maintainer; + break; + case WHEEL_META_MAINTAINER_EMAIL: + result.type = WHEELVAL_STRLIST; + result.data = meta->maintainer_email; + break; + case WHEEL_META_LICENSE: + result.data = meta->license; + break; + case WHEEL_META_HOME_PAGE: + result.data = meta->home_page; + break; + case WHEEL_META_DOWNLOAD_URL: + result.data = meta->download_url; + break; + case WHEEL_META_PROJECT_URL: + result.type = WHEELVAL_STRLIST; + result.data = meta->project_url; + break; + case WHEEL_META_CLASSIFIER: + result.type = WHEELVAL_STRLIST; + result.data = meta->classifier; + break; + case WHEEL_META_REQUIRES_PYTHON: + result.type = WHEELVAL_STRLIST; + result.data = meta->requires_python; + break; + case WHEEL_META_DESCRIPTION_CONTENT_TYPE: + result.data = meta->description_content_type; + break; + case WHEEL_META_LICENSE_FILE: + result.type = WHEELVAL_STRLIST; + result.data = meta->license_file; + break; + case WHEEL_META_LICENSE_EXPRESSION: + result.data = meta->license_expression; + break; + case WHEEL_META_IMPORT_NAME: + result.type = WHEELVAL_STRLIST; + result.data = meta->import_name; + break; + case WHEEL_META_IMPORT_NAMESPACE: + result.type = WHEELVAL_STRLIST; + result.data = meta->import_namespace; + break; + case WHEEL_META_REQUIRES_DIST: + result.type = WHEELVAL_STRLIST; + result.data = meta->requires_dist; + break; + case WHEEL_META_PROVIDES_DIST: + result.type = WHEELVAL_STRLIST; + result.data = meta->provides_dist; + break; + case WHEEL_META_PROVIDES_EXTRA: + result.type = WHEELVAL_OBJ_EXTRA; + result.data = (struct WheelMetadata_ProvidesExtra *) meta->provides_extra; + result.count = result.data != NULL ? (size_t) ((struct WheelMetadata_ProvidesExtra *) result.data)->count : 0; + break; + case WHEEL_META_OBSOLETES: + result.type = WHEELVAL_STRLIST; + result.data = meta->obsoletes; + break; + case WHEEL_META_OBSOLETES_DIST: + result.type = WHEELVAL_STRLIST; + result.data = meta->obsoletes_dist; + break; + case WHEEL_META_DESCRIPTION: + result.type = WHEELVAL_STR; + result.data = meta->description; + break; + case WHEEL_META_PLATFORM: + result.type = WHEELVAL_STRLIST; + result.data = meta->platform; + break; + case WHEEL_META_SUPPORTED_PLATFORM: + result.type = WHEELVAL_STRLIST; + result.data = meta->supported_platform; + break; + case WHEEL_META_KEYWORDS: + result.type = WHEELVAL_STRLIST; + result.data = meta->keywords; + break; + case WHEEL_META_DYNAMIC: + result.type = WHEELVAL_STRLIST; + result.data = meta->dynamic; + break; + case WHEEL_KEY_UNKNOWN: + default: + result.data = NULL; + break; + } + + switch (result.type) { + case WHEELVAL_STR: + result.count = result.data != NULL ? strlen(result.data) : 0; + break; + case WHEELVAL_STRLIST: + result.count = result.data != NULL ? strlist_count(result.data) : 0; + break; + default: + break; + } + + return result; +} + +static ssize_t get_key_index(const char **arr, const char *key) { + for (ssize_t i = 0; arr[i] != NULL; i++) { + if (strcmp(arr[i], key) == 0) { + return i; + } + } + return -1; +} + +const char *wheel_get_key_by_id(const int from, const ssize_t id) { + if (from == WHEEL_FROM_DIST) { + if (id >= 0 && id < WHEEL_DIST_END_ENUM) { + return WHEEL_DIST_KEY[id]; + } + } + if (from == WHEEL_FROM_METADATA) { + if (id >= 0 && id < WHEEL_META_END_ENUM) { + return WHEEL_META_KEY[id]; + } + } + return NULL; +} + +struct WheelValue wheel_get_value_by_name(const struct Wheel *pkg, const int from, const char *key) { + struct WheelValue result = {0}; + ssize_t id; + + if (from == WHEEL_FROM_DIST) { + id = get_key_index(WHEEL_DIST_KEY, key); + result = wheel_data_dump(pkg, id); + } else if (from == WHEEL_FROM_METADATA) { + id = get_key_index(WHEEL_META_KEY, key); + result = wheel_metadata_dump(pkg, id); + } else { + result.data = NULL; + result.type = WHEEL_KEY_UNKNOWN; + } + + return result; +} + +struct WheelValue wheel_get_value_by_id(const struct Wheel *pkg, const int from, const ssize_t id) { + struct WheelValue result = {0}; + + if (from == WHEEL_FROM_DIST) { + result = wheel_data_dump(pkg, id); + } else if (from == WHEEL_FROM_METADATA) { + result = wheel_metadata_dump(pkg, id); + } else { + result.data = NULL; + result.type = -1; + } + return result; } -void wheel_free(struct Wheel **wheel) { - struct Wheel *w = (*wheel); - if (!w) { - return; +void wheel_record_free(struct WheelRecord **record) { + guard_free((*record)->filename); + guard_free((*record)->checksum); + guard_free(*record); +} + +void wheel_entry_point_free(struct WheelEntryPoint **entry) { + guard_free((*entry)->name); + guard_free((*entry)->function); + guard_free((*entry)->type); + guard_free(*entry); +} + +void wheel_metadata_free(struct WheelMetadata *meta) { + guard_free(meta->license); + guard_free(meta->license_expression); + guard_free(meta->version); + guard_free(meta->name); + guard_free(meta->description); + guard_free(meta->metadata_version); + guard_free(meta->summary); + guard_free(meta->description_content_type); + guard_free(meta->home_page); + guard_free(meta->download_url); + + guard_strlist_free(&meta->author_email); + guard_strlist_free(&meta->author); + guard_strlist_free(&meta->maintainer); + guard_strlist_free(&meta->maintainer_email); + guard_strlist_free(&meta->requires_python); + guard_strlist_free(&meta->requires_external); + guard_strlist_free(&meta->project_url); + guard_strlist_free(&meta->classifier); + guard_strlist_free(&meta->requires_dist); + guard_strlist_free(&meta->keywords); + guard_strlist_free(&meta->license_file); + + for (size_t i = 0; meta->provides_extra[i] != NULL; i++) { + guard_free(meta->provides_extra[i]->target); + guard_strlist_free(&meta->provides_extra[i]->requires_dist); + guard_free(meta->provides_extra[i]); } - guard_free(w->path_name); - guard_free(w->file_name); - guard_free(w->distribution); - guard_free(w->version); - guard_free(w->build_tag); - guard_free(w->python_tag); - guard_free(w->abi_tag); - guard_free(w->python_tag); - guard_free(w->platform_tag); - guard_free(w); + guard_free(meta->provides_extra); + + guard_free(meta); } + +void wheel_package_free(struct Wheel **pkg) { + guard_free((*pkg)->wheel_version); + guard_free((*pkg)->generator); + guard_free((*pkg)->root_is_pure_lib); + wheel_metadata_free((*pkg)->metadata); + + guard_strlist_free(&(*pkg)->tag); + guard_strlist_free(&(*pkg)->top_level); + for (size_t i = 0; (*pkg)->record && (*pkg)->record[i] != NULL; i++) { + wheel_record_free(&(*pkg)->record[i]); + } + guard_free((*pkg)->record); + + for (size_t i = 0; (*pkg)->entry_point && (*pkg)->entry_point[i] != NULL; i++) { + wheel_entry_point_free(&(*pkg)->entry_point[i]); + } + guard_free((*pkg)->entry_point); + guard_free((*pkg)); +} + +int wheel_get_top_level(struct Wheel *pkg, const char *filename) { + char *data = NULL; + if (wheel_get_file_contents(filename, "*.dist-info/top_level.txt", &data)) { + guard_free(data); + return -1; + } + if (!pkg->top_level) { + pkg->top_level = strlist_init(); + } + strlist_append_tokenize(pkg->top_level, data, "\r\n"); + guard_free(data); + return 0; +} + +int wheel_get_zip_safe(struct Wheel *pkg, const char *filename) { + char *data = NULL; + const int exists = wheel_get_file_contents(filename, "*.dist-info/zip-safe", &data) == 0; + guard_free(data); + + pkg->zip_safe = 0; + if (exists) { + pkg->zip_safe = 1; + } + + return 0; +} + +int wheel_get_records(struct Wheel *pkg, const char *filename) { + char *data = NULL; + const int exists = wheel_get_file_contents(filename, "*.dist-info/RECORD", &data) == 0; + + if (!exists) { + guard_free(data); + return 1; + } + + const size_t records_initial_count = 2; + pkg->record = calloc(records_initial_count, sizeof(*pkg->record)); + if (!pkg->record) { + guard_free(data); + return 1; + } + size_t records_count = 0; + + const char *token = NULL; + char *data_orig = data; + while ((token = strsep(&data, "\r\n")) != NULL) { + if (!strlen(token)) { + continue; + } + + pkg->record[records_count] = calloc(1, sizeof(*pkg->record[0])); + if (!pkg->record[records_count]) { + return -1; + } + + struct WheelRecord *record = pkg->record[records_count]; + for (size_t x = 0; x < 3; x++) { + const char *next_comma = strpbrk(token, ","); + if (next_comma) { + if (x == 0) { + record->filename = strndup(token, next_comma - token); + } else if (x == 1) { + record->checksum = strndup(token, next_comma - token); + } + token = next_comma + 1; + } else { + record->size = strtol(token, NULL, 10); + } + } + records_count++; + + struct WheelRecord **tmp = realloc(pkg->record, (records_count + 2) * sizeof(*pkg->record)); + if (tmp == NULL) { + guard_free(data); + return -1; + } + pkg->record = tmp; + pkg->record[records_count + 1] = NULL; + } + + pkg->num_record = records_count; + guard_free(data_orig); + return 0; +} + +int wheel_get(struct Wheel **pkg, const char *filename) { + char *data = NULL; + if (wheel_get_file_contents(filename, "*.dist-info/WHEEL", &data) < 0) { + return -1; + } + const ssize_t result = wheel_parse_wheel(*pkg, data); + guard_free(data); + return (int) result; +} + +int wheel_get_entry_point(struct Wheel *pkg, const char *filename) { + char *data = NULL; + if (wheel_get_file_contents(filename, "*.dist-info/entry_points.txt", &data) < 0) { + return -1; + } + + struct StrList *lines = strlist_init(); + if (!lines) { + goto GEP_FAIL; + } + strlist_append_tokenize(lines, data, "\r\n"); + + const size_t line_count = strlist_count(lines); + size_t usable_lines = line_count; + for (size_t i = 0; i < line_count; i++) { + const char *item = strlist_item(lines, i); + if (isempty((char *) item) || item[0] == '[') { + usable_lines--; + } + } + + pkg->entry_point = calloc(usable_lines + 1, sizeof(*pkg->entry_point)); + if (!pkg->entry_point) { + goto GEP_FAIL; + } + + for (size_t i = 0; i < usable_lines; i++) { + pkg->entry_point[i] = calloc(1, sizeof(*pkg->entry_point[0])); + if (!pkg->entry_point[i]) { + goto GEP_FAIL; + } + } + + size_t x = 0; + char section[255] = {0}; + for (size_t i = 0; i < line_count; i++) { + const char *item = strlist_item(lines, i); + if (isempty((char *) item)) { + continue; + } + + if (strpbrk(item, "[")) { + const size_t start = strcspn((char *) item, "[") + 1; + if (start) { + const size_t len = strcspn((char *) item, "]"); + strncpy(section, item + start, len - start); + section[len - start] = '\0'; + continue; + } + } + + pkg->entry_point[x]->type = strdup(section); + if (!pkg->entry_point[x]->type) { + goto GEP_FAIL; + } + + char **pair = split((char *) item, "=", 1); + if (!pair) { + wheel_entry_point_free(&pkg->entry_point[x]); + goto GEP_FAIL; + } + + pkg->entry_point[x]->name = strdup(strip(pair[0])); + if (!pkg->entry_point[x]->name) { + wheel_entry_point_free(&pkg->entry_point[x]); + guard_array_free(pair); + goto GEP_FAIL; + } + + pkg->entry_point[x]->function = strdup(lstrip(pair[1])); + if (!pkg->entry_point[x]->function) { + wheel_entry_point_free(&pkg->entry_point[x]); + guard_array_free(pair); + goto GEP_FAIL; + } + guard_array_free(pair); + x++; + } + pkg->num_entry_point = x; + guard_strlist_free(&lines); + guard_free(data); + return 0; + + GEP_FAIL: + guard_strlist_free(&lines); + guard_free(data); + return -1; +} + +int wheel_value_error(struct WheelValue const *val) { + if (val) { + if (val->type < 0 && val->data == NULL) { + return 1; + } + } + return 0; +} + +int wheel_show_info(const struct Wheel *wheel) { + printf("WHEEL INFO\n\n"); + for (ssize_t i = 0; i < WHEEL_DIST_END_ENUM; i++) { + const char *key = wheel_get_key_by_id(WHEEL_FROM_DIST, i); + if (!key) { + fprintf(stderr, "wheel_get_key_by_id(%zi) failed\n", i); + return -1; + } + + printf("%s: ", key); + fflush(stdout); + const struct WheelValue dist = wheel_get_value_by_id(wheel, WHEEL_FROM_DIST, i); + if (wheel_value_error(&dist)) { + fprintf(stderr, "wheel_get_value_by_id(%zi) failed\n", i); + return -1; + } + switch (dist.type) { + case WHEELVAL_STR: { + char *s = dist.data; + if (s != NULL && !isempty(s)) { + printf("%s\n", s); + } else { + printf("[N/A]\n"); + } + break; + } + case WHEELVAL_STRLIST: { + struct StrList *list = dist.data; + if (list) { + printf("\n"); + for (size_t x = 0; x < strlist_count(list); x++) { + const char *item = strlist_item(list, x); + printf(" %s\n", item); + } + } else { + printf("[N/A]\n"); + } + break; + } + case WHEELVAL_OBJ_RECORD: { + struct WheelRecord **record = dist.data; + if (record && *record) { + printf("\n"); + for (size_t x = 0; x < dist.count; x++) { + printf(" [%zu] %s (size: %zu bytes, checksum: %s)\n", x, wheel->record[x]->filename, wheel->record[x]->size, strlen(wheel->record[x]->checksum) ? wheel->record[x]->checksum : "N/A"); + } + } else { + printf("[N/A]\n"); + } + break; + } + case WHEELVAL_OBJ_ENTRY_POINT: { + struct WheelEntryPoint **entry = dist.data; + if (entry && *entry) { + printf("\n"); + for (size_t x = 0; x < dist.count; x++) { + printf(" [%zu] type: %s, name: %s, function: %s\n", x, entry[x]->type, entry[x]->name, entry[x]->function); + } + } else { + printf("[N/A]\n"); + } + break; + } + default: + printf("[no handler]\n"); + break; + } + + } + + printf("\nPACKAGE INFO\n\n"); + for (ssize_t i = 0; i < WHEEL_META_END_ENUM; i++) { + const char *key = wheel_get_key_by_id(WHEEL_FROM_METADATA, i); + if (!key) { + fprintf(stderr, "wheel_get_key_by_id(%zi) failed\n", i); + return -1; + } + printf("%s: ", key); + fflush(stdout); + + const struct WheelValue pkg = wheel_get_value_by_id(wheel, WHEEL_FROM_METADATA, i); + if (wheel_value_error(&pkg)) { + fprintf(stderr, "wheel_get_value_by_id(%zi) failed\n", i); + return -1; + } + switch (pkg.type) { + case WHEELVAL_STR: { + char *s = pkg.data; + if (s != NULL && !isempty(s)) { + printf("%s\n", s); + } else { + printf("[N/A]\n"); + } + break; + } + case WHEELVAL_STRLIST: { + struct StrList *list = pkg.data; + if (list) { + printf("\n"); + for (size_t x = 0; x < strlist_count(list); x++) { + const char *item = strlist_item(list, x); + printf(" %s\n", item); + } + } else { + printf("[N/A]\n"); + } + break; + } + case WHEELVAL_OBJ_EXTRA: { + const struct WheelMetadata_ProvidesExtra **extra = pkg.data; + printf("\n"); + if (*extra) { + for (size_t x = 0; extra[x] != NULL; x++) { + printf(" + %s\n", extra[x]->target); + for (size_t z = 0; z < strlist_count(extra[x]->requires_dist); z++) { + const char *item = strlist_item(extra[x]->requires_dist, z); + printf(" `- %s\n", item); + } + } + } else { + printf("[N/A]\n"); + } + break; + } + default: + break; + } + } + return 0; +} + +int wheel_package(struct Wheel **pkg, const char *filename) { + if (!filename) { + return WHEEL_PACKAGE_E_FILENAME; + } + if (!*pkg) { + *pkg = calloc(1, sizeof(**pkg)); + if (!*pkg) { + return WHEEL_PACKAGE_E_ALLOC; + } + + (*pkg)->metadata = calloc(1, sizeof(*(*pkg)->metadata)); + if (!(*pkg)->metadata) { + guard_free(*pkg); + return WHEEL_PACKAGE_E_ALLOC; + } + } + if (wheel_get(pkg, filename) < 0) { + return WHEEL_PACKAGE_E_GET; + } + if (wheel_metadata_get(*pkg, filename) < 0) { + return WHEEL_PACKAGE_E_GET_METADATA; + } + if (wheel_get_top_level(*pkg, filename) < 0) { + return WHEEL_PACKAGE_E_GET_TOP_LEVEL; + } + if (wheel_get_records(*pkg, filename) < 0) { + return WHEEL_PACKAGE_E_GET_RECORDS; + } + if (wheel_get_entry_point(*pkg, filename) < 0) { + return WHEEL_PACKAGE_E_GET_ENTRY_POINT; + } + + // Optional marker + wheel_get_zip_safe(*pkg, filename); + + return WHEEL_PACKAGE_E_SUCCESS; +} + + diff --git a/src/lib/core/wheelinfo.c b/src/lib/core/wheelinfo.c new file mode 100644 index 0000000..1a93a82 --- /dev/null +++ b/src/lib/core/wheelinfo.c @@ -0,0 +1,129 @@ +#include "wheelinfo.h" + +struct WheelInfo *wheelinfo_get(const char *basepath, const char *name, char *to_match[], unsigned match_mode) { + struct dirent *rec; + struct WheelInfo *result = NULL; + char package_path[PATH_MAX]; + char package_name[NAME_MAX]; + + strcpy(package_name, name); + tolower_s(package_name); + sprintf(package_path, "%s/%s", basepath, package_name); + + DIR *dp = opendir(package_path); + if (!dp) { + return NULL; + } + + while ((rec = readdir(dp)) != NULL) { + if (!strcmp(rec->d_name, ".") || !strcmp(rec->d_name, "..")) { + continue; + } + char filename[NAME_MAX]; + strcpy(filename, rec->d_name); + char *ext = strstr(filename, ".whl"); + if (ext) { + *ext = '\0'; + } else { + // not a wheel file. nothing to do + continue; + } + + size_t match = 0; + size_t pattern_count = 0; + for (; to_match[pattern_count] != NULL; pattern_count++) { + if (strstr(filename, to_match[pattern_count])) { + match++; + } + } + + if (!startswith(rec->d_name, name)) { + continue; + } + + if (match_mode == WHEEL_MATCH_EXACT && match != pattern_count) { + continue; + } + + result = calloc(1, sizeof(*result)); + if (!result) { + SYSERROR("Unable to allocate %zu bytes for wheel struct", sizeof(*result)); + closedir(dp); + return NULL; + } + + result->path_name = realpath(package_path, NULL); + if (!result->path_name) { + SYSERROR("Unable to resolve absolute path to %s: %s", filename, strerror(errno)); + wheelinfo_free(&result); + closedir(dp); + return NULL; + } + result->file_name = strdup(rec->d_name); + if (!result->file_name) { + SYSERROR("Unable to allocate bytes for %s: %s", rec->d_name, strerror(errno)); + wheelinfo_free(&result); + closedir(dp); + return NULL; + } + + size_t parts_total; + char **parts = split(filename, "-", 0); + if (!parts) { + // This shouldn't happen unless a wheel file is present in the + // directory with a malformed file name, or we've managed to + // exhaust the system's memory + SYSERROR("%s has no '-' separators! (Delete this file and try again)", filename); + wheelinfo_free(&result); + closedir(dp); + return NULL; + } + + for (parts_total = 0; parts[parts_total] != NULL; parts_total++) {} + if (parts_total == 5) { + // no build tag + result->distribution = strdup(parts[0]); + result->version = strdup(parts[1]); + result->build_tag = NULL; + result->python_tag = strdup(parts[2]); + result->abi_tag = strdup(parts[3]); + result->platform_tag = strdup(parts[4]); + } else if (parts_total == 6) { + // has build tag + result->distribution = strdup(parts[0]); + result->version = strdup(parts[1]); + result->build_tag = strdup(parts[2]); + result->python_tag = strdup(parts[3]); + result->abi_tag = strdup(parts[4]); + result->platform_tag = strdup(parts[5]); + } else { + SYSERROR("Unknown wheel name format: %s. Expected 5 or 6 strings " + "separated by '-', but got %zu instead", filename, parts_total); + guard_array_free(parts); + wheelinfo_free(&result); + closedir(dp); + return NULL; + } + guard_array_free(parts); + break; + } + closedir(dp); + return result; +} + +void wheelinfo_free(struct WheelInfo **wheel) { + struct WheelInfo *w = (*wheel); + if (!w) { + return; + } + guard_free(w->path_name); + guard_free(w->file_name); + guard_free(w->distribution); + guard_free(w->version); + guard_free(w->build_tag); + guard_free(w->python_tag); + guard_free(w->abi_tag); + guard_free(w->python_tag); + guard_free(w->platform_tag); + guard_free(w); +} diff --git a/src/lib/delivery/delivery_init.c b/src/lib/delivery/delivery_init.c index eb0b527..8a0bb61 100644 --- a/src/lib/delivery/delivery_init.c +++ b/src/lib/delivery/delivery_init.c @@ -119,7 +119,7 @@ void delivery_init_dirs_stage1(struct Delivery *ctx) { } if (access(ctx->storage.mission_dir, F_OK)) { - msg(STASIS_MSG_L1, "%s: %s\n", ctx->storage.mission_dir, strerror(errno)); + msg(STASIS_MSG_L1, "%s: %s: mission directory does not exist\n", ctx->storage.mission_dir, strerror(errno)); exit(1); } diff --git a/src/lib/delivery/delivery_install.c b/src/lib/delivery/delivery_install.c index f40a509..fbd1b8f 100644 --- a/src/lib/delivery/delivery_install.c +++ b/src/lib/delivery/delivery_install.c @@ -252,7 +252,7 @@ int delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, cha } strlist_append_tokenize(tag_data, info->repository_info_tag, "-"); - struct Wheel *whl = NULL; + struct WheelInfo *whl = NULL; char *post_commit = NULL; char *hash = NULL; if (strlist_count(tag_data) > 1) { @@ -264,7 +264,7 @@ int delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, cha // equal to the tag; setuptools_scm auto-increments the value, the user can change it manually, // etc. errno = 0; - whl = get_wheel_info(ctx->storage.wheel_artifact_dir, info->name, + whl = wheelinfo_get(ctx->storage.wheel_artifact_dir, info->name, (char *[]) {ctx->meta.python_compact, ctx->system.arch, "none", "any", post_commit, hash, @@ -282,7 +282,7 @@ int delivery_install_packages(struct Delivery *ctx, char *conda_install_dir, cha info->version = strdup(whl->version); } guard_strlist_free(&tag_data); - wheel_free(&whl); + wheelinfo_free(&whl); } char req[255] = {0}; diff --git a/src/lib/delivery/include/delivery.h b/src/lib/delivery/include/delivery.h index cae4b02..ddc819e 100644 --- a/src/lib/delivery/include/delivery.h +++ b/src/lib/delivery/include/delivery.h @@ -18,7 +18,7 @@ #include "ini.h" #include "multiprocessing.h" #include "recipe.h" -#include "wheel.h" +#include "wheelinfo.h" #define DELIVERY_PLATFORM_MAX 4 #define DELIVERY_PLATFORM_MAXLEN 65 diff --git a/tests/test_wheel.c b/tests/test_wheel.c index 6818b22..1eabb1b 100644 --- a/tests/test_wheel.c +++ b/tests/test_wheel.c @@ -1,91 +1,216 @@ +#include "delivery.h" #include "testing.h" +#include "str.h" #include "wheel.h" -void test_get_wheel_file() { - struct testcase { - const char *filename; - struct Wheel expected; - }; - struct testcase tc[] = { - { - // Test for "build tags" - .filename = "btpackage-1.2.3-mytag-py2.py3-none-any.whl", - .expected = { - .file_name = "btpackage-1.2.3-mytag-py2.py3-none-any.whl", - .version = "1.2.3", - .distribution = "btpackage", - .build_tag = "mytag", - .platform_tag = "any", - .python_tag = "py2.py3", - .abi_tag = "none", - .path_name = ".", - } - }, - { - // Test for universal package format - .filename = "anypackage-1.2.3-py2.py3-none-any.whl", - .expected = { - .file_name = "anypackage-1.2.3-py2.py3-none-any.whl", - .version = "1.2.3", - .distribution = "anypackage", - .build_tag = NULL, - .platform_tag = "any", - .python_tag = "py2.py3", - .abi_tag = "none", - .path_name = ".", - } - }, - { - // Test for binary package format - .filename = "binpackage-1.2.3-cp311-cp311-linux_x86_64.whl", - .expected = { - .file_name = "binpackage-1.2.3-cp311-cp311-linux_x86_64.whl", - .version = "1.2.3", - .distribution = "binpackage", - .build_tag = NULL, - .platform_tag = "linux_x86_64", - .python_tag = "cp311", - .abi_tag = "cp311", - .path_name = ".", - } - }, - }; +char cwd_start[PATH_MAX]; +char cwd_workspace[PATH_MAX]; +int conda_is_installed = 0; +static char conda_prefix[PATH_MAX] = {0}; +struct Delivery ctx; +static const char *testpkg_filename = "testpkg/dist/testpkg-1.0.0-py3-none-any.whl"; + + +static void test_wheel_package() { + const char *filename = testpkg_filename; + struct Wheel *wheel = NULL; + int state = wheel_package(&wheel, filename); + STASIS_ASSERT(state != WHEEL_PACKAGE_E_ALLOC, "Cannot fail to allocate memory for package structure"); + STASIS_ASSERT(state != WHEEL_PACKAGE_E_GET, "Cannot fail to parse wheel"); + STASIS_ASSERT(state != WHEEL_PACKAGE_E_GET_METADATA, "Cannot fail to read wheel metadata"); + STASIS_ASSERT(state != WHEEL_PACKAGE_E_GET_RECORDS, "Cannot fail reading wheel path records"); + STASIS_ASSERT(state != WHEEL_PACKAGE_E_GET_ENTRY_POINT, "Cannot fail reading wheel entry points"); + STASIS_ASSERT(state == WHEEL_PACKAGE_E_SUCCESS, "Wheel file should be usable"); + STASIS_ASSERT(wheel != NULL, "wheel cannot be NULL"); + STASIS_ASSERT(wheel != NULL, "wheel_package failed to initialize wheel struct"); + STASIS_ASSERT(wheel->record != NULL, "Record cannot be NULL"); + STASIS_ASSERT(wheel->num_record > 0, "Record count cannot be zero"); + STASIS_ASSERT(wheel->tag != NULL, "Package tag list cannot be NULL"); + STASIS_ASSERT(wheel->generator != NULL, "Generator field cannot be NULL"); + STASIS_ASSERT(wheel->top_level != NULL, "Top level directory name cannot be NULL"); + STASIS_ASSERT(wheel->wheel_version != NULL, "Wheel version cannot be NULL"); + STASIS_ASSERT(wheel->metadata != NULL, "Metadata cannot be NULL"); + STASIS_ASSERT(wheel->metadata->name != NULL, "Metadata::name cannot be NULL"); + STASIS_ASSERT(wheel->metadata->version != NULL, "Metadata::version cannot be NULL"); + STASIS_ASSERT(wheel->metadata->metadata_version != NULL, "Metadata::version (of metadata) cannot be NULL"); + + // Implied test against key/id getters. If wheel_show_info segfaults, that functionality is broken. + STASIS_ASSERT(wheel_show_info(wheel) == 0, "wheel_show_info should never fail. Enum(s) might be out of sync with META_*_KEYS array(s)"); + + // Get data from DIST + const struct WheelValue dist_version = wheel_get_value_by_name(wheel, WHEEL_FROM_DIST, "Wheel-Version"); + STASIS_ASSERT(dist_version.type == WHEELVAL_STR, "Wheel dist version value must be a string"); + STASIS_ASSERT_FATAL(dist_version.data != NULL, "Wheel dist version value must not be NULL"); + STASIS_ASSERT(dist_version.count, "Wheel value must be populated"); + + // Get data from METADATA + const struct WheelValue meta_name = wheel_get_value_by_name(wheel, WHEEL_FROM_METADATA, "Metadata-Version"); + STASIS_ASSERT(meta_name.type == WHEELVAL_STR, "Wheel metadata version value must be a string"); + STASIS_ASSERT_FATAL(meta_name.data != NULL, "Wheel metadata version value must not be NULL"); + STASIS_ASSERT(meta_name.count, "Wheel metadata version value must be populated"); + + wheel_package_free(&wheel); + STASIS_ASSERT(wheel == NULL, "wheel struct should be NULL after free"); +} + +static void mock_python_package() { + const char *pyproject_toml_data = "[build-system]\n" + "requires = [\"setuptools >= 77.0.3\"]\n" + "build-backend = \"setuptools.build_meta\"\n" + "\n" + "[project]\n" + "name = \"testpkg\"\n" + "version = \"1.0.0\"\n" + "authors = [{name = \"STASIS Team\", email = \"stasis@not-a-real-domain.tld\"}]\n" + "description = \"A STASIS test package\"\n" + "readme = \"README.md\"\n" + "license = \"BSD-3-Clause\"\n" + "classifiers = [\"Programming Language :: Python :: 3\"]\n" + "\n" + "[project.urls]\n" + "Homepage = \"https://not-a-real-address.tld\"\n" + "Documentation = \"https://not-a-real-address.tld/docs\"\n" + "Repository = \"https://not-a-real-address.tld/repo.git\"\n" + "Issues = \"https://not-a-real-address.tld/tracker\"\n" + "Changelog = \"https://not-a-real-address.tld/changes\"\n"; + const char *readme = "# testpkg\n\nThis is a test package, for testing.\n"; - struct Wheel *doesnotexist = get_wheel_info("doesnotexist", "doesnotexist-0.0.1-py2.py3-none-any.whl", (char *[]) {"not", NULL}, WHEEL_MATCH_ANY); - STASIS_ASSERT(doesnotexist == NULL, "returned non-NULL on error"); - - for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { - struct testcase *test = &tc[i]; - struct Wheel *wheel = get_wheel_info(".", test->expected.distribution, (char *[]) {(char *) test->expected.version, NULL}, WHEEL_MATCH_ANY); - STASIS_ASSERT(wheel != NULL, "result should not be NULL!"); - STASIS_ASSERT(wheel->file_name && strcmp(wheel->file_name, test->expected.file_name) == 0, "mismatched file name"); - STASIS_ASSERT(wheel->version && strcmp(wheel->version, test->expected.version) == 0, "mismatched version"); - STASIS_ASSERT(wheel->distribution && strcmp(wheel->distribution, test->expected.distribution) == 0, "mismatched distribution (package name)"); - STASIS_ASSERT(wheel->platform_tag && strcmp(wheel->platform_tag, test->expected.platform_tag) == 0, "mismatched platform tag ([platform]_[architecture])"); - STASIS_ASSERT(wheel->python_tag && strcmp(wheel->python_tag, test->expected.python_tag) == 0, "mismatched python tag (python version)"); - STASIS_ASSERT(wheel->abi_tag && strcmp(wheel->abi_tag, test->expected.abi_tag) == 0, "mismatched abi tag (python compatibility version)"); - if (wheel->build_tag) { - STASIS_ASSERT(strcmp(wheel->build_tag, test->expected.build_tag) == 0, - "mismatched build tag (optional arbitrary string)"); - } - wheel_free(&wheel); + mkdir("testpkg", 0755); + mkdir("testpkg/src", 0755); + mkdir("testpkg/src/testpkg", 0755); + if (touch("testpkg/src/testpkg/__init__.py")) { + fprintf(stderr, "unable to write __init__.py"); + exit(1); + } + if (touch("testpkg/README.md")) { + fprintf(stderr, "unable to write README.md"); + exit(1); + } + if (stasis_testing_write_ascii("testpkg/pyproject.toml", pyproject_toml_data)) { + perror("unable to write pyproject.toml"); + exit(1); + } + if (stasis_testing_write_ascii("testpkg/README.md", readme)) { + perror("unable to write readme"); + exit(1); + } + if (pip_exec("install build")) { + fprintf(stderr, "unable to install build tool using pip\n"); + exit(1); + } + if (python_exec("-m build -w ./testpkg")) { + fprintf(stderr, "unable build test package"); + exit(1); } } int main(int argc, char *argv[]) { STASIS_TEST_BEGIN_MAIN(); STASIS_TEST_FUNC *tests[] = { - test_get_wheel_file, + test_wheel_package, }; - // Create mock package directories, and files - mkdir("binpackage", 0755); - touch("binpackage/binpackage-1.2.3-cp311-cp311-linux_x86_64.whl"); - mkdir("anypackage", 0755); - touch("anypackage/anypackage-1.2.3-py2.py3-none-any.whl"); - mkdir("btpackage", 0755); - touch("btpackage/btpackage-1.2.3-mytag-py2.py3-none-any.whl"); + char ws[] = "workspace_XXXXXX"; + if (!mkdtemp(ws)) { + perror("mkdtemp"); + exit(1); + } + getcwd(cwd_start, sizeof(cwd_start) - 1); + mkdir(ws, 0755); + chdir(ws); + getcwd(cwd_workspace, sizeof(cwd_workspace) - 1); + + snprintf(conda_prefix, strlen(cwd_workspace) + strlen("conda") + 2, "%s/conda", cwd_workspace); + + const char *mockinidata = "[meta]\n" + "name = mock\n" + "version = 1.0.0\n" + "rc = 1\n" + "mission = generic\n" + "python = 3.11\n" + "[conda]\n" + "installer_name = Miniforge3\n" + "installer_version = 24.3.0-0\n" + "installer_platform = {{env:STASIS_CONDA_PLATFORM}}\n" + "installer_arch = {{env:STASIS_CONDA_ARCH}}\n" + "installer_baseurl = https://github.com/conda-forge/miniforge/releases/download/24.3.0-0\n"; + stasis_testing_write_ascii("mock.ini", mockinidata); + struct INIFILE *ini = ini_open("mock.ini"); + ctx._stasis_ini_fp.delivery = ini; + ctx._stasis_ini_fp.delivery_path = realpath("mock.ini", NULL); + + const char *sysconfdir = getenv("STASIS_SYSCONFDIR"); + globals.sysconfdir = strdup(sysconfdir ? sysconfdir : STASIS_SYSCONFDIR); + ctx.storage.root = strdup(cwd_workspace); + char *cfgfile = join((char *[]) {globals.sysconfdir, "stasis.ini", NULL}, "/"); + if (!cfgfile) { + perror("unable to create path to global config"); + exit(1); + } + + ctx._stasis_ini_fp.cfg = ini_open(cfgfile); + if (!ctx._stasis_ini_fp.cfg) { + fprintf(stderr, "unable to open config file, %s\n", cfgfile); + exit(1); + } + ctx._stasis_ini_fp.cfg_path = realpath(cfgfile, NULL); + if (!ctx._stasis_ini_fp.cfg_path) { + fprintf(stderr, "unable to determine absolute path of config, %s\n", cfgfile); + exit(1); + } + guard_free(cfgfile); + + setenv("LANG", "C", 1); + if (bootstrap_build_info(&ctx)) { + fprintf(stderr, "bootstrap_build_info failed\n"); + exit(1); + } + if (delivery_init(&ctx, INI_READ_RENDER)) { + fprintf(stderr, "delivery_init failed\n"); + exit(1); + } + + char *install_url = calloc(255, sizeof(install_url)); + delivery_get_conda_installer_url(&ctx, install_url); + delivery_get_conda_installer(&ctx, install_url); + delivery_install_conda(ctx.conda.installer_path, ctx.storage.conda_install_prefix); + guard_free(install_url); + + if (conda_activate(ctx.storage.conda_install_prefix, "base")) { + fprintf(stderr, "conda_activate failed\n"); + exit(1); + } + if (conda_exec("install -y boa conda-build")) { + fprintf(stderr, "conda_exec failed\n"); + exit(1); + } + if (conda_setup_headless()) { + fprintf(stderr, "conda_setup_headless failed\n"); + exit(1); + } + if (conda_env_create("testpkg", ctx.meta.python, NULL)) { + fprintf(stderr, "conda_env_create failed\n"); + exit(1); + } + if (conda_activate(ctx.storage.conda_install_prefix, "testpkg")) { + fprintf(stderr, "conda_activate failed\n"); + exit(1); + } + + mock_python_package(); STASIS_TEST_RUN(tests); + + if (chdir(cwd_start) < 0) { + fprintf(stderr, "chdir failed\n"); + exit(1); + } + if (rmtree(cwd_workspace)) { + perror(cwd_workspace); + } + delivery_free(&ctx); + globals_free(); + STASIS_TEST_END_MAIN(); + }
\ No newline at end of file diff --git a/tests/test_wheelinfo.c b/tests/test_wheelinfo.c new file mode 100644 index 0000000..1abbeac --- /dev/null +++ b/tests/test_wheelinfo.c @@ -0,0 +1,91 @@ +#include "testing.h" +#include "wheelinfo.h" + +void test_wheelinfo_get() { + struct testcase { + const char *filename; + struct WheelInfo expected; + }; + struct testcase tc[] = { + { + // Test for "build tags" + .filename = "btpackage-1.2.3-mytag-py2.py3-none-any.whl", + .expected = { + .file_name = "btpackage-1.2.3-mytag-py2.py3-none-any.whl", + .version = "1.2.3", + .distribution = "btpackage", + .build_tag = "mytag", + .platform_tag = "any", + .python_tag = "py2.py3", + .abi_tag = "none", + .path_name = ".", + } + }, + { + // Test for universal package format + .filename = "anypackage-1.2.3-py2.py3-none-any.whl", + .expected = { + .file_name = "anypackage-1.2.3-py2.py3-none-any.whl", + .version = "1.2.3", + .distribution = "anypackage", + .build_tag = NULL, + .platform_tag = "any", + .python_tag = "py2.py3", + .abi_tag = "none", + .path_name = ".", + } + }, + { + // Test for binary package format + .filename = "binpackage-1.2.3-cp311-cp311-linux_x86_64.whl", + .expected = { + .file_name = "binpackage-1.2.3-cp311-cp311-linux_x86_64.whl", + .version = "1.2.3", + .distribution = "binpackage", + .build_tag = NULL, + .platform_tag = "linux_x86_64", + .python_tag = "cp311", + .abi_tag = "cp311", + .path_name = ".", + } + }, + }; + + struct WheelInfo *doesnotexist = wheelinfo_get("doesnotexist", "doesnotexist-0.0.1-py2.py3-none-any.whl", (char *[]) {"not", NULL}, WHEEL_MATCH_ANY); + STASIS_ASSERT(doesnotexist == NULL, "returned non-NULL on error"); + + for (size_t i = 0; i < sizeof(tc) / sizeof(*tc); i++) { + struct testcase *test = &tc[i]; + struct WheelInfo *wheel = wheelinfo_get(".", test->expected.distribution, (char *[]) {(char *) test->expected.version, NULL}, WHEEL_MATCH_ANY); + STASIS_ASSERT(wheel != NULL, "result should not be NULL!"); + STASIS_ASSERT(wheel->file_name && strcmp(wheel->file_name, test->expected.file_name) == 0, "mismatched file name"); + STASIS_ASSERT(wheel->version && strcmp(wheel->version, test->expected.version) == 0, "mismatched version"); + STASIS_ASSERT(wheel->distribution && strcmp(wheel->distribution, test->expected.distribution) == 0, "mismatched distribution (package name)"); + STASIS_ASSERT(wheel->platform_tag && strcmp(wheel->platform_tag, test->expected.platform_tag) == 0, "mismatched platform tag ([platform]_[architecture])"); + STASIS_ASSERT(wheel->python_tag && strcmp(wheel->python_tag, test->expected.python_tag) == 0, "mismatched python tag (python version)"); + STASIS_ASSERT(wheel->abi_tag && strcmp(wheel->abi_tag, test->expected.abi_tag) == 0, "mismatched abi tag (python compatibility version)"); + if (wheel->build_tag) { + STASIS_ASSERT(strcmp(wheel->build_tag, test->expected.build_tag) == 0, + "mismatched build tag (optional arbitrary string)"); + } + wheelinfo_free(&wheel); + } +} + +int main(int argc, char *argv[]) { + STASIS_TEST_BEGIN_MAIN(); + STASIS_TEST_FUNC *tests[] = { + test_wheelinfo_get, + }; + + // Create mock package directories, and files + mkdir("binpackage", 0755); + touch("binpackage/binpackage-1.2.3-cp311-cp311-linux_x86_64.whl"); + mkdir("anypackage", 0755); + touch("anypackage/anypackage-1.2.3-py2.py3-none-any.whl"); + mkdir("btpackage", 0755); + touch("btpackage/btpackage-1.2.3-mytag-py2.py3-none-any.whl"); + + STASIS_TEST_RUN(tests); + STASIS_TEST_END_MAIN(); +}
\ No newline at end of file |
