/** * MIT/X Consortium License * * Copyright © 2015 Mattias Andrée * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #define t(...) do { if (__VA_ARGS__) goto fail; } while (0) /** * Default value for the environment variable LIBRARIAN_PATH. */ #ifndef DEFAULT_PATH # define DEFAULT_PATH "/usr/local/share/librarian:/usr/share/librarian" #endif /** * A library and version range. */ struct library { /** * The name of the library. */ const char *name; /** * The lowest acceptable version. * `NULL` if unbounded. */ char *lower; /** * The highest acceptable version. * `NULL` if unbounded. */ char *upper; /** * Is the version stored in * `lower` acceptable. */ int lower_closed; /** * Is the version stored in * `ypper` acceptable. */ int upper_closed; }; /** * Structure for already located librarian files. */ struct found_file { /** * The name of the library. */ const char *name; /** * The found version of the library. */ char *version; /** * The path name of the librarian file. */ char *path; }; /** * The name of the process. */ static const char *argv0; /** * Sorted list of already located librarian files. */ static struct found_file *found_files = NULL; /** * The number of elements in `found_files`. */ static size_t found_files_count = 0; /** * Compares the name of two libraries. * * @param a:const struct library * One of the libraries. * @param b:const struct library * The other library. * @return <0: `a` < `b`. * =0: `a` = `b`. * >0: `a` > `b`. */ static int library_name_cmp(const void *a, const void *b) { const struct library *la = a; const struct library *lb = b; return strcmp(la->name, lb->name); } /** * Compares the name of two `struct found_file`. * * @param a:const struct found_file * One of the files. * @param b:const struct found_file * The other file. * @return <0: `a` < `b`. * =0: `a` = `b`. * >0: `a` > `b`. */ static int found_file_name_cmp(const void *a, const void *b) { const struct found_file *fa = a; const struct found_file *fb = b; return strcmp(fa->name, fb->name); } /** * Determine whether a string is the * name of a non-reserved variable. * * @param s The string. * @return 1: The string is a varible name. * 0: The string is a library. */ static int is_variable(const char *s) { for (; *s; s++) { if (isupper(*s)) ; else if (isdigit(*s)) ; else if (strchr("_-", *s)) ; else return 0; } return 1; } /** * Parse a library–library-version range * argument. * * @param s The string. * @param lib Output parameter for the library spec:s. * @return 0: Successful. * 1: Syntax error. */ static int parse_library(char *s, struct library *lib) { char *p; char c; memset(lib, 0, sizeof(*lib)); if (strchr(s, '/') || strchr("<>=", *s)) return 1; lib->name = s; p = strpbrk(s, "<>="); if (p == NULL) return 0; c = *p, *p++ = '\0'; switch (c) { case '=': lib->lower_closed = lib->upper_closed = 1; lib->lower = lib->upper = p; break; case '>': p += lib->lower_closed = (*p == '='); lib->lower = p; s = strchr(p, '<'); if (s == NULL) break; *s++ = '\0'; if (!*(p = s)) return 0; /* fall through */ case '<': p += lib->upper_closed = (*p == '='); lib->upper = p; break; default: assert(0); abort(); break; } return (strpbrk(p, "<>=") || !*p); } /** * Compare two version numbers that do not * compare any does, and does not have an * epoch. * * @param a One of the version numbers. * @param b The other version number. * @return <0: `a` < `b`. * =0: `a` = `b`. * >1: `a` > `b`. */ static int version_subcmp(char *a, char *b) { char *ap; char *bp; char ac, bc; int r; while (*a || *b) { /* Compare digit part. */ /* (We must support arbitrary lenght.) */ ap = a + strspn(a, "0123456789"); bp = b + strspn(b, "0123456789"); while (*a == '0') a++; while (*b == '0') b++; ac = *ap, bc = *bp; *ap = '\0', *bp = '\0'; if (ap - a < bp - b) return -1; if (ap - a > bp - b) return +1; r = strcmp(a, b); *ap = ac, *bp = bc; a = *a ? (ap + 1) : a; b = *b ? (bp + 1) : b; if (r) return r; /* Compare letter (non-digit) part. */ ap = a + strcspn(a, "0123456789"); bp = b + strcspn(b, "0123456789"); ac = *ap, bc = *bp; *ap = '\0', *bp = '\0'; r = strcmp(a, b); *ap = ac, *bp = bc; a = *a ? (ap + 1) : a; b = *b ? (bp + 1) : b; if (r) return r; } return 0; } /** * Compare two version numbers. * * @param a One of the version numbers. * @param b The other version number. * @return <0: `a` < `b`. * =0: `a` = `b`. * >0: `a` > `b`. */ static int version_cmp(char *a, char *b) { #define COMPARE \ if (ap && bp) { \ ac = *ap, bc = *bp; \ *ap = *bp = '\0'; \ r = version_subcmp(a, b); \ *ap = ac, *bp = bc; \ a = ap + 1, b = bp + 1; \ } else if (ap) { \ ac = *ap, *ap = '\0'; \ r = version_subcmp(a, nil); \ *ap = ac, a = ap + 1; \ } else if (bp) { \ bc = *bp, *bp = '\0'; \ r = version_subcmp(nil, b); \ *bp = bc, b = bp + 1; \ } \ if (r) return r char *ap; char *bp; char ac, bc; int r = 0; static char nil[1] = { '\0' }; /* Compare epoch. */ ap = strchr(a, ':'); bp = strchr(b, ':'); COMPARE; /* Compare non-epoch */ for (;;) { ap = strchr(a, '.'); bp = strchr(b, '.'); if (!ap && !ap) return 0; COMPARE; } #undef COMPARE } /** * Test whether a version of a library is compatible. * * @param version The found version. * @param required Compatible version range. * @return 1: Version is accepted. * 0: Version is incompatible. */ static int test_library_version(char *version, struct library *required) { int upper = required->upper ? version_cmp(version, required->upper) : -1; int lower = required->lower ? version_cmp(version, required->lower) : +1; upper = required->lower_closed ? (upper <= 0) : (upper < 0); lower = required->lower_closed ? (lower >= 0) : (lower > 0); return upper && lower; } /** * Locate a librarian file in a directory. * * @param lib Library specification. * @param path The pathname of the directory. * @param oldest Are older versions prefered? * @return The pathname of the library's librarian file. * `NULL` on error or if not found, if not found, * `errno` is set to 0. */ static char *locate_in_dir(struct library *lib, char *path, int oldest) { DIR *d = NULL; struct dirent *f; char *p; char *old = NULL; char *best = NULL; char *best_ver; int r, saved_errno; d = opendir(path); t (d == NULL); while ((f = (errno = 0, readdir(d)))) { p = strrchr(f->d_name, '='); if (p == NULL) continue; *p = '\0'; if (strcmp(f->d_name, lib->name)) continue; *p++ = '='; if (!test_library_version(p, lib)) continue; if (best == NULL) { old = best, best = strdup(f->d_name); } else { best_ver = strrchr(best, '='); assert(best_ver && !strchr(best_ver, '/')); r = version_cmp(p, best_ver + 1); if (oldest ? (r < 0) : (r > 0)) old = best, best = strdup(f->d_name); } if (best == NULL) { best = old; goto fail; } free(old); } t (errno); closedir(d), d = NULL; if (best == NULL) return errno = 0, NULL; p = malloc(strlen(path) + strlen(best) + 2); t (p == NULL); stpcpy(stpcpy(stpcpy(p, path), "/"), best); return p; fail: saved_errno = errno; free(best); if (d != NULL) closedir(d); errno = saved_errno; return NULL; } /** * Locate a librarian file on the system. * * @param lib Library specification. * @param path LIBRARIAN_PATH. * @param oldest Are older versions prefered? * @return The pathname of the library's librarian file. * `NULL` on error or if not found, if not found, * `errno` is set to 0. */ static char *locate(struct library *lib, char *path, int oldest) { char *p; char *end = path; char *e; char *best = NULL; char *found; char *old; char *best_ver; char *found_ver; int r, saved_errno; for (p = path; end; *e = (end ? ':' : '\0'), p = end + 1) { end = strchr(p, ':'); e = end ? end : strchr(p, '\0'); *e = '\0'; if (!*p) continue; found = locate_in_dir(lib, p, oldest); if (found == NULL) { t (errno); continue; } old = found; if (best == NULL) { old = best, best = found; } else { best_ver = strrchr(best, '='); found_ver = strrchr(found, '='); assert(best_ver && !strchr(best_ver, '/')); assert(found_ver && !strchr(found_ver, '/')); r = version_cmp(found_ver + 1, best_ver + 1); if (oldest ? (r < 0) : (r > 0)) old = best, best = found; } free(old); } return errno = 0, best; fail: saved_errno = errno; free(best); errno = saved_errno; return NULL; } /** * Find librarian files for all libraries. * * Found files are appended to `found_files`. * * @param libraries The sought libraries. * @param n The number of elements in `libraries`. * @param path LIBRARIAN_PATH. * @param oldest Are older versions prefered? * @return 0: Successful and found all files. * -1 and !errno: Did not find all files, but otherwise successful. * -1 and errno: An error occurred */ static int find_librarian_files(struct library *libraries, size_t n, char *path, int oldest) { size_t i; char *best = NULL; char *found; char *best_ver; char *found_ver; const char *last = NULL; void *new; size_t ffc = found_files_count; int r; struct found_file f; struct found_file *have; qsort(libraries, n, sizeof(*libraries), library_name_cmp); qsort(found_files, ffc, sizeof(*found_files), found_file_name_cmp); new = realloc(found_files, (ffc + n) * sizeof(*found_files)); t (new == NULL); found_files = new; for (i = 0; i < n; i++) { f.name = libraries[i].name; have = bsearch(&f, found_files, ffc, sizeof(*found_files), found_file_name_cmp); if (have) { if (test_library_version(have->version, libraries + i)) continue; goto not_this_range; } found = locate(libraries + i, path, oldest); t (!found && errno); if (found == NULL) goto not_this_range; if (last && !strcmp(f.name, last)) { best_ver = strrchr(best, '='); found_ver = strrchr(found, '='); assert(best_ver && !strchr(best_ver, '/')); assert(found_ver && !strchr(found_ver, '/')); r = version_cmp(found + 1, best_ver + 1); r = oldest ? (r < 0) : (r > 0); if (r) free(best); } else { r = 1, found_files_count++; } if (r) { found_ver = strrchr(found, '='); assert(found_ver && !strchr(found_ver, '/')); found_files[found_files_count - 1].name = f.name; found_files[found_files_count - 1].version = found_ver + 1; found_files[found_files_count - 1].path = best = found; } last = f.name; continue; not_this_range: if (i + 1 == n) goto not_found; if (strcmp(f.name, libraries[i + 1].name)) goto not_found; continue; } return 0; not_found: if (libraries[i].upper == libraries[i].lower) { fprintf(stderr, "%s: cannot find library: %s%s%s", argv0, libraries[i].name, libraries[i].upper ? "=" : "", libraries[i].upper); } else { fprintf(stderr, "%s: cannot find library: %s%s%s%s%s%s%s", argv0, libraries[i].name, libraries[i].lower ? ">" : "", libraries[i].lower_closed ? "=" : "", libraries[i].lower ? libraries[i].lower : "", libraries[i].upper ? "<" : "", libraries[i].upper_closed ? "=" : "", libraries[i].upper ? libraries[i].upper : ""); } errno = 0; fail: return -1; } /** * @return 0: Program was successful. * 1: An error occurred. * 2: A library was not found. * 3: Usage error. */ int main(int argc, char *argv[]) { int dashed = 0, f_deps = 0, f_locate = 0, f_oldest = 0; char *arg; char **args = argv; char **args_last = argv; char **variables = argv; char **variables_last = argv; struct library *libraries = NULL; struct library *libraries_last; const char *path_; char *path = NULL; int rc; /* Parse arguments. */ argv0 = argv ? (argc--, *argv++) : "pp"; while (argc) { if (!dashed && !strcmp(*argv, "--")) { dashed = 1; argv++; argc--; } else if (!dashed && (**argv == '-')) { arg = *argv++; argc--; if (!*arg) goto usage; for (arg++; *arg; arg++) { if (*arg == 'd') f_deps = 1; else if (*arg == 'l') f_locate = 1; else if (*arg == 'o') f_oldest = 1; else goto usage; } } else { *args_last++ = *argv++; argc--; } } if (f_deps && f_locate) goto usage; /* Parse VARIABLE and LIBRARY arguments. */ libraries = malloc((size_t)(args_last - args) * sizeof(*libraries)); libraries_last = libraries; t (libraries == NULL); for (; args != args_last; args++) { if (is_variable(*args)) *variables_last = *args; else if (parse_library(*args, libraries_last++)) goto usage; } /* Get LIBRARIAN_PATH. */ path_ = getenv("LIBRARIAN_PATH"); if (!path_ || !*path_) path_ = DEFAULT_PATH; path = strdup(path_); t (path == NULL); /* Find librarian files. */ if (find_librarian_files(libraries, (size_t)(libraries_last - libraries), path, f_oldest)) { t (errno); goto not_found; } if (f_locate) { while (found_files_count) t (printf("%s\n", found_files[--found_files_count].path) < 0); goto done; } /* TODO */ done: rc = 0; goto cleanup; fail: perror(argv0); rc = 1; goto cleanup; not_found: rc = 2; goto cleanup; usage: fprintf(stderr, "%s: Invalid arguments, see `man 1 librarian'.\n", argv0); rc = 3; goto cleanup; cleanup: while (found_files_count--) free(found_files[found_files_count].path); free(found_files); free(libraries); free(path); return rc; }