diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | config.m4 | 28 | ||||
-rw-r--r-- | package.xml | 115 | ||||
-rw-r--r-- | php_rpminfo.h | 14 | ||||
-rw-r--r-- | rpminfo.c | 488 | ||||
-rw-r--r-- | rpminfo.stub.php | 17 | ||||
-rw-r--r-- | rpminfo_arginfo.h | 60 | ||||
-rw-r--r-- | tests/002-rpmvercmp.phpt | 44 | ||||
-rw-r--r-- | tests/003-rpminfo.phpt | 2 | ||||
-rw-r--r-- | tests/005-rpminfo-full.phpt | 6 | ||||
-rw-r--r-- | tests/010-rpmvercmp_error7.phpt | 25 | ||||
-rw-r--r-- | tests/011-rpmvercmp_error8.phpt | 12 | ||||
-rw-r--r-- | tests/012-rpmaddtag.phpt | 16 | ||||
-rw-r--r-- | tests/013-rpmdbsearch-error.phpt | 16 | ||||
-rw-r--r-- | tests/014-stream.phpt | 97 | ||||
-rw-r--r-- | tests/bidon.rpm | bin | 6972 -> 7599 bytes | |||
-rw-r--r-- | tests/bidon.spec | 42 |
17 files changed, 825 insertions, 158 deletions
@@ -1,6 +1,7 @@ # Object files *.o *.lo +*.dep # Libraries *.lib @@ -4,29 +4,19 @@ PHP_ARG_ENABLE(rpminfo, whether to enable rpminfo support, [ --enable-rpminfo Enable rpminfo support]) if test "$PHP_RPMINFO" != "no"; then - dnl Write more examples of tests here... - AC_PATH_PROG(PKG_CONFIG, pkg-config, no) + PKG_CHECK_MODULES([LIBRPM], [rpm >= 4.11.3]) - AC_MSG_CHECKING(for librpm) - if test -x "$PKG_CONFIG" && $PKG_CONFIG --exists rpm; then - if $PKG_CONFIG rpm --atleast-version 4.11.3; then - LIBRPM_CFLAGS=`$PKG_CONFIG rpm --cflags` - LIBRPM_LIBDIR=`$PKG_CONFIG rpm --libs` - LIBRPM_VERSON=`$PKG_CONFIG rpm --modversion` - AC_MSG_RESULT(from pkgconfig: version $LIBRPM_VERSON) - if $PKG_CONFIG rpm --atleast-version 4.13; then - AC_DEFINE(HAVE_WEAKDEP, 1, [ Indexes on weak dependency field since RPM 4.13 ]) - fi - else - AC_MSG_ERROR(system librpm is too old: version 4.11.3 required) - fi - else - AC_MSG_ERROR(pkg-config not found) - fi - PHP_EVAL_LIBLINE($LIBRPM_LIBDIR, RPMINFO_SHARED_LIBADD) + PHP_EVAL_LIBLINE($LIBRPM_LIBS, RPMINFO_SHARED_LIBADD) PHP_EVAL_INCLINE($LIBRPM_CFLAGS) + AC_MSG_CHECKING(for rpm >= 4.13) + PKG_CHECK_EXISTS([rpm >= 4.13], + AC_DEFINE(HAVE_ARCHIVE, 1, [ Archive reader since RPM 4.13 ]) + AC_DEFINE(HAVE_WEAKDEP, 1, [ Indexes on weak dependency field since RPM 4.13 ]) + AC_MSG_RESULT([yes]), AC_MSG_RESULT([no]) + ) + PHP_SUBST(RPMINFO_SHARED_LIBADD) PHP_NEW_EXTENSION(rpminfo, rpminfo.c, $ext_shared,, -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1) diff --git a/package.xml b/package.xml index 237bd54..ff6a59e 100644 --- a/package.xml +++ b/package.xml @@ -13,18 +13,19 @@ Documentation: https://www.php.net/rpminfo <email>remi@php.net</email> <active>yes</active> </lead> - <date>2020-04-07</date> + <date>2023-11-10</date> <version> - <release>0.5.1dev</release> - <api>0.5.0</api> + <release>1.1.1dev</release> + <api>1.1.0</api> </version> <stability> <release>stable</release> <api>stable</api> </stability> - <license>PHP 3.01</license> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> <notes> -- split tests for PHP 7/8 +- check open_basedir restriction +- new function: rpmgetsymlink(string $path, string $name): ?string </notes> <contents> <dir name="/"> @@ -32,6 +33,8 @@ Documentation: https://www.php.net/rpminfo <file name="config.m4" role="src"/> <file name="php_rpminfo.h" role="src" /> <file name="rpminfo.c" role="src"/> + <file name="rpminfo.stub.php" role="src"/> + <file name="rpminfo_arginfo.h" role="src"/> <!-- documentation --> <file name="CREDITS" role="doc"/> <file name="LICENSE" role="doc"/> @@ -51,8 +54,10 @@ Documentation: https://www.php.net/rpminfo <file name="007-rpmdbinfo.phpt" role="test"/> <file name="008-rpmdbsearch.phpt" role="test"/> <file name="009-rpmdbinfo2.phpt" role="test"/> - <file name="010-rpmvercmp_error7.phpt" role="test"/> <file name="011-rpmvercmp_error8.phpt" role="test"/> + <file name="012-rpmaddtag.phpt" role="test"/> + <file name="013-rpmdbsearch-error.phpt" role="test"/> + <file name="014-stream.phpt" role="test"/> <file name="bidon.rpm" role="test"/> <file name="bidon-src.rpm" role="test"/> </dir> @@ -61,7 +66,7 @@ Documentation: https://www.php.net/rpminfo <dependencies> <required> <php> - <min>7.0.0</min> + <min>8.0.0</min> </php> <pearinstaller> <min>1.10.0</min> @@ -72,6 +77,102 @@ Documentation: https://www.php.net/rpminfo <extsrcrelease/> <changelog> <release> + <date>2023-11-10</date> + <version> + <release>1.1.0</release> + <api>1.1.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> + <notes> +- check open_basedir restriction +- new function: rpmgetsymlink(string $path, string $name): ?string + </notes> + </release> + <release> + <date>2023-10-13</date> + <version> + <release>1.0.1</release> + <api>1.0.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> + <notes> +- fix stack smashing on 32-bit +- allow retrieval of hardlink content + </notes> + </release> + <release> + <date>2023-10-12</date> + <version> + <release>1.0.0</release> + <api>1.0.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> + <notes> +- implement rpm stream wrapper with librpm >= 4.13 + </notes> + </release> + <release> + <date>2023-09-26</date> + <version> + <release>0.7.0</release> + <api>0.7.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> + <notes> +- add optional operator to rpmcmpver for consistency with version_compare +- drop support for PHP 7 + </notes> + </release> + <release> + <date>2021-06-18</date> + <version> + <release>0.6.0</release> + <api>0.5.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license uri="https://www.php.net/license/3_01.txt" filesource="LICENSE">PHP-3.01</license> + <notes> +- generate arginfo from stub and add missing default values +- raise dependency on PHP 7.2 +- raise exception on bad parameter value with PHP 8 + </notes> + </release> + <release> + <date>2020-09-23</date> + <version> + <release>0.5.1</release> + <api>0.5.0</api> + </version> + <stability> + <release>stable</release> + <api>stable</api> + </stability> + <license>PHP 3.01</license> + <notes> +- split tests for PHP 7/8 +- improve librpm example + </notes> + </release> + <release> <date>2020-04-07</date> <version> <release>0.5.0</release> diff --git a/php_rpminfo.h b/php_rpminfo.h index 2ba34ea..ddc912f 100644 --- a/php_rpminfo.h +++ b/php_rpminfo.h @@ -1,6 +1,6 @@ /* +----------------------------------------------------------------------+ - | PHP Version 7 | + | rpminfo extension for PHP | +----------------------------------------------------------------------+ | Copyright (c) The PHP Group | +----------------------------------------------------------------------+ @@ -22,15 +22,9 @@ extern zend_module_entry rpminfo_module_entry; #define phpext_rpminfo_ptr &rpminfo_module_entry -#define PHP_RPMINFO_VERSION "0.5.1-dev" - -#ifdef PHP_WIN32 -# define PHP_RPMINFO_API __declspec(dllexport) -#elif defined(__GNUC__) && __GNUC__ >= 4 -# define PHP_RPMINFO_API __attribute__ ((visibility("default"))) -#else -# define PHP_RPMINFO_API -#endif +#define PHP_RPMINFO_VERSION "1.1.1-dev" +#define PHP_RPMINFO_AUTHOR "Remi Collet" +#define PHP_RPMINFO_LICENSE "PHP-3.01" #ifdef ZTS #include "TSRM.h" @@ -1,6 +1,6 @@ /* +----------------------------------------------------------------------+ - | PHP Version 7 | + | rpminfo extension for PHP | +----------------------------------------------------------------------+ | Copyright (c) The PHP Group | +----------------------------------------------------------------------+ @@ -23,6 +23,7 @@ #include "php.h" #include "php_ini.h" #include "ext/standard/info.h" +#include "ext/standard/php_string.h" #include <fcntl.h> #include <rpm/rpmdb.h> @@ -32,6 +33,21 @@ #include "php_rpminfo.h" +#include "rpminfo_arginfo.h" + +#ifdef HAVE_ARCHIVE +struct php_rpm_stream_data_t { + FD_t gzdi; + Header h; + rpmfiles files; + rpmfi fi; + php_stream *stream; +}; + +#define STREAM_DATA_FROM_STREAM() \ + struct php_rpm_stream_data_t *self = (struct php_rpm_stream_data_t *) stream->abstract; +#endif + ZEND_DECLARE_MODULE_GLOBALS(rpminfo) static rpmts rpminfo_getts(void) { @@ -199,16 +215,6 @@ static void rpm_header_to_zval(zval *return_value, Header h, zend_bool full) } } -#if PHP_VERSION_ID < 70200 -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpminfo, 0, 1, IS_ARRAY, NULL, 1) -#else -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpminfo, 0, 1, IS_ARRAY, 1) -#endif - ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, full, _IS_BOOL, 0) - ZEND_ARG_TYPE_INFO(1, error, IS_STRING, 1) -ZEND_END_ARG_INFO() - /* {{{ proto array rpminfo(string path [, bool full [, string &$error]) Retrieve information from a RPM file */ PHP_FUNCTION(rpminfo) @@ -222,13 +228,16 @@ PHP_FUNCTION(rpminfo) rpmts ts = rpminfo_getts(); if (zend_parse_parameters(ZEND_NUM_ARGS(), "p|bz", &path, &len, &full, &error) == FAILURE) { - return; + RETURN_THROWS(); } if (error) { ZVAL_DEREF(error); zval_dtor(error); ZVAL_NULL(error); } + if (php_check_open_basedir(path)) { + RETURN_NULL(); + } f = Fopen(path, "r"); if (f) { @@ -267,15 +276,6 @@ PHP_FUNCTION(rpminfo) } /* }}} */ -#if PHP_VERSION_ID < 70200 -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbinfo, 0, 1, IS_ARRAY, NULL, 1) -#else -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbinfo, 0, 1, IS_ARRAY, 1) -#endif - ZEND_ARG_TYPE_INFO(0, nevr, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, full, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - /* {{{ proto array rpmdbinfo(string nevr [, bool full]) Retrieve information from an installed RPM */ PHP_FUNCTION(rpmdbinfo) @@ -288,13 +288,13 @@ PHP_FUNCTION(rpmdbinfo) rpmdbMatchIterator di; if (zend_parse_parameters(ZEND_NUM_ARGS(), "p|b", &name, &len, &full) == FAILURE) { - return; + RETURN_THROWS(); } db = rpminfo_getdb(); di = rpmdbInitIterator(db, RPMDBI_LABEL, name, len); if (!di) { - // Not found + /* Not found */ RETURN_NULL(); } @@ -319,7 +319,7 @@ static unsigned char nibble(char c) { if (c >= 'A' && c <= 'F') { return (c - 'A') + 10; } - return 0; + return 0; } static int hex2bin(const char *hex, char *bin, int len) { @@ -363,17 +363,6 @@ static int haveIndex(zend_long tag) { return 0; } -#if PHP_VERSION_ID < 70200 -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbsearch, 0, 1, IS_ARRAY, NULL, 1) -#else -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbsearch, 0, 1, IS_ARRAY, 1) -#endif - ZEND_ARG_TYPE_INFO(0, pattern, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, rpmtag, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, rpmmire, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, full, _IS_BOOL, 0) -ZEND_END_ARG_INFO() - /* {{{ proto array rpmdbsearch(string pattern [, integer tag_name = RPMTAG_NAME [, integer mode = -1 [, bool full = 0]]]) Search information from installed RPMs */ PHP_FUNCTION(rpmdbsearch) @@ -391,20 +380,32 @@ PHP_FUNCTION(rpmdbsearch) int useIndex = 1; if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|llb", &name, &len, &crit, &mode, &full) == FAILURE) { - return; + RETURN_THROWS(); + } + if (rpmTagGetType(crit) == RPM_NULL_TYPE) { + zend_argument_value_error(2, "Unkown rpmtag"); + RETURN_THROWS(); + } + if (mode != RPMMIRE_DEFAULT && + mode != RPMMIRE_STRCMP && + mode != RPMMIRE_REGEX && + mode != RPMMIRE_GLOB && + mode != -1) { + zend_argument_value_error(3, "Unkown rpmmire"); + RETURN_THROWS(); } if (crit == RPMTAG_PKGID) { if (len != 32) { - php_error_docref(NULL, E_WARNING, "Bad length for PKGID, 32 expected"); - RETURN_NULL(); + zend_argument_value_error(1, "Bad length for PKGID, 32 expected"); + RETURN_THROWS(); } len = hex2bin(name, MD5, len); name = MD5; } else if (crit == RPMTAG_HDRID) { if (len != 40) { - php_error_docref(NULL, E_WARNING, "Bad length for HDRID, 40 expected"); - RETURN_NULL(); + zend_argument_value_error(1, "Bad length for HDRID, 40 expected"); + RETURN_THROWS(); } } else if (crit == RPMTAG_INSTALLTID) { tid = atol(name); @@ -433,7 +434,7 @@ PHP_FUNCTION(rpmdbsearch) } } if (!di) { - // Not found + /* Not found */ RETURN_NULL(); } @@ -448,15 +449,6 @@ PHP_FUNCTION(rpmdbsearch) } /* }}} */ -#if PHP_VERSION_ID < 70200 -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmvercmp, 0, 2, IS_LONG, NULL, 0) -#else -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmvercmp, 0, 2, IS_LONG, 0) -#endif - ZEND_ARG_TYPE_INFO(0, evr1, IS_STRING, 0) - ZEND_ARG_TYPE_INFO(0, evr2, IS_STRING, 0) -ZEND_END_ARG_INFO() - /* {{{ proto int rpmcmpver(string evr1, string evr2) Compare 2 RPM EVRs (epoch:version-release) strings */ PHP_FUNCTION(rpmvercmp) @@ -464,16 +456,17 @@ PHP_FUNCTION(rpmvercmp) char *in_evr1, *evr1, *v1, *r1; char *in_evr2, *evr2, *v2, *r2; char *p, empty[] = ""; + char *op = NULL; long e1, e2, r; - size_t len1, len2; + size_t len1, len2, oplen; - if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &in_evr1, &len1, &in_evr2, &len2) == FAILURE) { - return; + if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss|s", &in_evr1, &len1, &in_evr2, &len2, &op, &oplen) == FAILURE) { + RETURN_THROWS(); } evr1 = estrdup(in_evr1); evr2 = estrdup(in_evr2); - // Epoch + /* Epoch */ p = strchr(evr1, ':'); if (p) { v1 = p+1; @@ -493,11 +486,11 @@ PHP_FUNCTION(rpmvercmp) e2 = 0; } if (e1 < e2) { - RETVAL_LONG(-1); + r = -1; } else if (e1 > e2) { - RETVAL_LONG(1); + r = 1; } else { - // Version + /* Version */ p = strchr(v1, '-'); if (p) { r1 = p+1; @@ -513,27 +506,42 @@ PHP_FUNCTION(rpmvercmp) r2 = empty; } r = rpmvercmp(v1, v2); - if (r) { - RETVAL_LONG(r); - } else { - // Release + if (!r) { + /* Release*/ r = rpmvercmp(r1, r2); - RETVAL_LONG(r); } } efree(evr1); efree(evr2); + + if (!op) { + RETURN_LONG(r); + } + + if (!strcmp(op, "<") || !strcmp(op, "lt")) { + RETURN_BOOL(r < 0); + } + if (!strcmp(op, "<=") || !strcmp(op, "le")) { + RETURN_BOOL(r <= 0); + } + if (!strcmp(op, ">") || !strcmp(op, "gt")) { + RETURN_BOOL(r > 0); + } + if (!strcmp(op, ">=") || !strcmp(op, "ge")) { + RETURN_BOOL(r >= 0); + } + if (!strcmp(op, "==") || !strcmp(op, "=") || !strcmp(op, "eq")) { + RETURN_BOOL(r == 0); + } + if (!strcmp(op, "!=") || !strcmp(op, "<>") || !strcmp(op, "ne")) { + RETURN_BOOL(r != 0); + } + + zend_argument_value_error(3, "must be a valid comparison operator"); + RETURN_THROWS(); } /* }}} */ -#if PHP_VERSION_ID < 70200 -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmaddtag, 0, 1, _IS_BOOL, NULL, 0) -#else -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmaddtag, 0, 1, _IS_BOOL, 0) -#endif - ZEND_ARG_TYPE_INFO(0, rpmtag, IS_LONG, 0) -ZEND_END_ARG_INFO() - /* {{{ proto int rpmaddtag(int tag) add a tag in the default set */ PHP_FUNCTION(rpmaddtag) @@ -542,7 +550,12 @@ PHP_FUNCTION(rpmaddtag) zend_long tag; if (zend_parse_parameters(ZEND_NUM_ARGS(), "l", &tag) == FAILURE) { - return; + RETURN_THROWS(); + } + + if (rpmTagGetType(tag) == RPM_NULL_TYPE) { + zend_argument_value_error(1, "Unkown rpmtag"); + RETURN_THROWS(); } if (RPMINFO_G(tags)) { @@ -551,8 +564,10 @@ PHP_FUNCTION(rpmaddtag) RETURN_BOOL(0); } } - RPMINFO_G(max_tags) += 16; - RPMINFO_G(tags) = erealloc(RPMINFO_G(tags), RPMINFO_G(max_tags) * sizeof(rpmTagVal)); + if (RPMINFO_G(nb_tags) == RPMINFO_G(max_tags)) { + RPMINFO_G(max_tags) += 16; + RPMINFO_G(tags) = erealloc(RPMINFO_G(tags), RPMINFO_G(max_tags) * sizeof(rpmTagVal)); + } } else { RPMINFO_G(max_tags) = 16; RPMINFO_G(tags) = emalloc(RPMINFO_G(max_tags) * sizeof(rpmTagVal)); @@ -563,12 +578,308 @@ PHP_FUNCTION(rpmaddtag) } /* }}} */ +#ifdef HAVE_ARCHIVE +static ssize_t php_rpm_ops_read(php_stream *stream, char *buf, size_t count) +{ + ssize_t n = -1; + STREAM_DATA_FROM_STREAM(); + + if (self) { + n = rpmfiArchiveRead(self->fi, buf, count); + if (n == 0 || n < (ssize_t)count) { + stream->eof = 1; + } + } + return n; +} + +static void php_rpm_ops_free(struct php_rpm_stream_data_t *self, int close_handle) +{ + if (self) { + if (close_handle) { + Fclose(self->gzdi); + rpmfilesFree(self->files); + rpmfiFree(self->fi); + headerFree(self->h); + } + efree(self); + } +} + +static int php_rpm_ops_close(php_stream *stream, int close_handle) +{ + STREAM_DATA_FROM_STREAM(); + + php_rpm_ops_free(self, close_handle); + stream->abstract = NULL; + + return EOF; +} + +static int php_zip_ops_stat(php_stream *stream, php_stream_statbuf *ssb) +{ + STREAM_DATA_FROM_STREAM(); + + if (self) { + struct stat s[2]; /* librpm may use different size (32-bit) */ + int rc; + rc = rpmfiStat(self->fi, 0, s); + memcpy(&ssb->sb, s, sizeof(ssb->sb)); + return rc; + } + return -1; +} + +const php_stream_ops php_stream_rpmio_ops = { + NULL, /* write */ + php_rpm_ops_read, + php_rpm_ops_close, + NULL, /* flush */ + "rpm", + NULL, /* seek */ + NULL, /* cast */ + php_zip_ops_stat, + NULL /* set_option */ +}; + +static struct php_rpm_stream_data_t *php_stream_rpm_finder(const char *path, int want_content) +{ + size_t path_len; + zend_string *file_basename; + char file_dirname[MAXPATHLEN]; + char *fragment; + size_t fragment_len; + struct php_rpm_stream_data_t *self = NULL; + FD_t fdi; + FD_t gzdi; + int rc; + Header h; + char rpmio_flags[80]; + const char *compr; + rpmfiles files; + rpmfi fi; + rpmts ts = rpminfo_getts(); + + fragment = strchr(path, '#'); + if (!fragment) { + return NULL; + } + if (strncasecmp("rpm://", path, 6) == 0) { + path += 6; + } + fragment_len = strlen(fragment); + if (fragment_len < 1) { + return NULL; + } + path_len = strlen(path); + if (path_len >= MAXPATHLEN) { + return NULL; + } + memcpy(file_dirname, path, path_len - fragment_len); + file_dirname[path_len - fragment_len] = '\0'; + file_basename = php_basename(path, path_len - fragment_len, NULL, 0); + fragment++; + if (php_check_open_basedir(file_dirname)) { + zend_string_release_ex(file_basename, 0); + return NULL; + } + fdi = Fopen(file_dirname, "r.ufdio"); + if (Ferror(fdi)) { + zend_string_release_ex(file_basename, 0); + return NULL; + } + rc = rpmReadPackageFile(ts, fdi, "rpm2cpio", &h); + if (rc != RPMRC_OK && rc != RPMRC_NOKEY && rc != RPMRC_NOTTRUSTED) { + zend_string_release_ex(file_basename, 0); + Fclose(fdi); + return NULL; + } + + compr = headerGetString(h, RPMTAG_PAYLOADCOMPRESSOR); + snprintf(rpmio_flags, sizeof(rpmio_flags), "r.%s", compr ? compr : "gzip"); + gzdi = Fdopen(fdi, rpmio_flags); + if (gzdi == NULL) { + headerFree(h); + Fclose(fdi); + zend_string_release_ex(file_basename, 0); + return NULL; + } + + files = rpmfilesNew(NULL, h, 0, RPMFI_KEEPHEADER); + fi = rpmfiNewArchiveReader(gzdi, files, RPMFI_ITER_READ_ARCHIVE); + + while((rc = rpmfiNext(fi)) >=0) { + const char *fn = rpmfiFN(fi); + /* + printf("Name=%s, Size=%d, N=%d, mode=%d, reg=%d, content=%d, rdev=%d, inode=%d link=%s\n", fn, + (int)rpmfiFSize(fi), (int)rpmfiFNlink(fi), (int)rpmfiFMode(fi), + (int)S_ISREG(rpmfiFMode(fi)), (int)rpmfiArchiveHasContent(fi), + (int)rpmfiFRdev(fi), (int)rpmfiFInode(fi), rpmfiFLink(fi)); + */ + if (!strcmp(fn, fragment)) { + if (want_content && S_ISREG(rpmfiFMode(fi)) && !rpmfiArchiveHasContent(fi)) { + rpm_rdev_t rdev = rpmfiFRdev(fi); + rpm_ino_t inode = rpmfiFInode(fi); + while((rc = rpmfiNext(fi)) >=0) { + if (rdev == rpmfiFRdev(fi) && inode == rpmfiFInode(fi) && rpmfiArchiveHasContent(fi)) { + break; + } + } + } + break; + } + } + if (rc == RPMERR_ITER_END) { + Fclose(gzdi); + rpmfilesFree(files); + rpmfiFree(fi); + headerFree(h); + } else { + self = emalloc(sizeof(*self)); + self->gzdi = gzdi; + self->files = files; + self->fi = fi; + self->h = h; + } + zend_string_release_ex(file_basename, 0); + + return self; +} + +php_stream *php_stream_rpm_opener(php_stream_wrapper *wrapper, + const char *path, + const char *mode, + int options, + zend_string **opened_path, + php_stream_context *context STREAMS_DC) +{ + struct php_rpm_stream_data_t *self; + + if (mode[0] != 'r') { + return NULL; + } + self = php_stream_rpm_finder(path, 1); + if (self) { + if (opened_path) { + *opened_path = zend_string_init(path, strlen(path), 0); + } + if (!S_ISREG(rpmfiFMode(self->fi)) || !rpmfiArchiveHasContent(self->fi)) { + php_rpm_ops_free(self, 1); + } else { + return php_stream_alloc(&php_stream_rpmio_ops, self, NULL, mode); + } + } + + return NULL; +} + +static int php_stream_rpm_stat(php_stream_wrapper *wrapper, const char *url, int flags, + php_stream_statbuf *ssb, php_stream_context *context) +{ + struct php_rpm_stream_data_t *self; + int rc = -1; + + self = php_stream_rpm_finder(url, 0); + if (self) { + struct stat s[2]; /* librpm may use different size (32-bit) */ + rc = rpmfiStat(self->fi, 0, s); + memcpy(&ssb->sb, s, sizeof(ssb->sb)); + php_rpm_ops_free(self, 1); + } + + return rc; +} + +static const php_stream_wrapper_ops rpm_stream_wops = { + php_stream_rpm_opener, + NULL, /* close */ + NULL, /* fstat */ + php_stream_rpm_stat, + NULL, /* opendir */ + "RPM wrapper", + NULL, /* unlink */ + NULL, /* rename */ + NULL, /* mkdir */ + NULL, /* rmdir */ + NULL /* metadata */ +}; + +const php_stream_wrapper php_stream_rpm_wrapper = { + &rpm_stream_wops, + NULL, + 0 /* is_url */ +}; + +/* {{{ proto array rpmgetsymlink(string path , string name) + Retrieve soft link target of en entry */ +PHP_FUNCTION(rpmgetsymlink) +{ + char *path, *name; + const char *link; + size_t plen, nlen; + FD_t fdi; + FD_t gzdi; + int rc; + Header h; + char rpmio_flags[80]; + const char *compr; + rpmfiles files; + rpmfi fi; + rpmts ts = rpminfo_getts(); + + if (zend_parse_parameters(ZEND_NUM_ARGS(), "pp", &path, &plen, &name, &nlen) == FAILURE) { + RETURN_THROWS(); + } + + if (php_check_open_basedir(path)) { + RETURN_NULL(); + } + fdi = Fopen(path, "r.ufdio"); + if (Ferror(fdi)) { + RETURN_NULL(); + } + rc = rpmReadPackageFile(ts, fdi, "rpm2cpio", &h); + if (rc != RPMRC_OK && rc != RPMRC_NOKEY && rc != RPMRC_NOTTRUSTED) { + Fclose(fdi); + RETURN_NULL(); + } + + compr = headerGetString(h, RPMTAG_PAYLOADCOMPRESSOR); + snprintf(rpmio_flags, sizeof(rpmio_flags), "r.%s", compr ? compr : "gzip"); + gzdi = Fdopen(fdi, rpmio_flags); + if (gzdi == NULL) { + headerFree(h); + Fclose(fdi); + RETURN_NULL(); + } + + files = rpmfilesNew(NULL, h, 0, RPMFI_KEEPHEADER); + fi = rpmfiNewArchiveReader(gzdi, files, RPMFI_ITER_READ_ARCHIVE); + + rc = rpmfiFindFN(fi, name); + rpmfiSetFX(fi, rc); /* return value have change in 4.18 (from previous to new) */ + if (rc < 0 + || rpmfiFX(fi) != rc + || (link = rpmfiFLink(fi)) == NULL) { + RETVAL_NULL(); + } else { + RETVAL_STRING(link); + } + rpmfiFree(fi); + rpmfilesFree(files); + headerFree(h); + Fclose(gzdi); +} +/* }}} */ +#endif /* HAVE_ARCHIVE */ + + /* {{{ PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(rpminfo) { - const char *tagname; - rpmtd names; + const char *tagname; + rpmtd names; REGISTER_STRING_CONSTANT("RPMVERSION", (char *)RPMVERSION, CONST_CS | CONST_PERSISTENT); @@ -608,6 +919,10 @@ PHP_MINIT_FUNCTION(rpminfo) } rpmtdFree(names); +#ifdef HAVE_ARCHIVE + php_register_url_stream_wrapper("rpm", &php_stream_rpm_wrapper); +#endif + return SUCCESS; } /* }}} */ @@ -659,7 +974,14 @@ PHP_MINFO_FUNCTION(rpminfo) php_info_print_table_start(); php_info_print_table_header(2, "rpminfo support", "enabled"); php_info_print_table_row(2, "Extension version", PHP_RPMINFO_VERSION); + php_info_print_table_row(2, "Author", PHP_RPMINFO_AUTHOR); + php_info_print_table_row(2, "License", PHP_RPMINFO_LICENSE); php_info_print_table_row(2, "RPM library version", RPMVERSION); +#ifdef HAVE_ARCHIVE + php_info_print_table_row(2, "RPM stream wrapper", "yes"); +#else + php_info_print_table_row(2, "RPM stream wrapper", "no"); +#endif php_info_print_table_end(); /* Remove comments if you have entries in php.ini @@ -680,20 +1002,6 @@ static PHP_GINIT_FUNCTION(rpminfo) /* {{{ */ } /* }}} */ -/* {{{ rpminfo_functions[] - * - * Every user visible function must have an entry in rpminfo_functions[]. - */ -const zend_function_entry rpminfo_functions[] = { - PHP_FE(rpmaddtag, arginfo_rpmaddtag) - PHP_FE(rpmdbinfo, arginfo_rpmdbinfo) - PHP_FE(rpmdbsearch, arginfo_rpmdbsearch) - PHP_FE(rpminfo, arginfo_rpminfo) - PHP_FE(rpmvercmp, arginfo_rpmvercmp) - PHP_FE_END -}; -/* }}} */ - /* {{{ rpminfo_module_entry */ zend_module_entry rpminfo_module_entry = { @@ -701,7 +1009,7 @@ zend_module_entry rpminfo_module_entry = { NULL, NULL, "rpminfo", - rpminfo_functions, + ext_functions, PHP_MINIT(rpminfo), NULL, PHP_RINIT(rpminfo), diff --git a/rpminfo.stub.php b/rpminfo.stub.php new file mode 100644 index 0000000..7e6e32e --- /dev/null +++ b/rpminfo.stub.php @@ -0,0 +1,17 @@ +<?php + +/** @generate-function-entries */ + +function rpmaddtag(int $rpmtag): bool {} + +function rpmdbinfo(string $nevr, bool $full = false): Array|null {} + +function rpmdbsearch(string $pattern, int $rpmtag = RPMTAG_NAME, int $rpmmire = -1, bool $full = false): Array|null {} + +function rpminfo(string $path, bool $full = false, ?string &$error = null): Array|null {} + +function rpmvercmp(string $evr1, string $evr2, ?string $operator = null): int|bool {} + +#ifdef HAVE_ARCHIVE +function rpmgetsymlink(string $path, string $name): string|null {} +#endif diff --git a/rpminfo_arginfo.h b/rpminfo_arginfo.h new file mode 100644 index 0000000..aa0e1d3 --- /dev/null +++ b/rpminfo_arginfo.h @@ -0,0 +1,60 @@ +/* This is a generated file, edit the .stub.php file instead. + * Stub hash: e65e33b4f6ebedcc7ef3030714ebf7e4c06f2778 */ + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmaddtag, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, rpmtag, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbinfo, 0, 1, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO(0, nevr, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, full, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmdbsearch, 0, 1, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO(0, pattern, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, rpmtag, IS_LONG, 0, "RPMTAG_NAME") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, rpmmire, IS_LONG, 0, "-1") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, full, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpminfo, 0, 1, IS_ARRAY, 1) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, full, _IS_BOOL, 0, "false") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(1, error, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_rpmvercmp, 0, 2, MAY_BE_LONG|MAY_BE_BOOL) + ZEND_ARG_TYPE_INFO(0, evr1, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, evr2, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, operator, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +#if defined(HAVE_ARCHIVE) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_rpmgetsymlink, 0, 2, IS_STRING, 1) + ZEND_ARG_TYPE_INFO(0, path, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 0) +ZEND_END_ARG_INFO() +#endif + + +ZEND_FUNCTION(rpmaddtag); +ZEND_FUNCTION(rpmdbinfo); +ZEND_FUNCTION(rpmdbsearch); +ZEND_FUNCTION(rpminfo); +ZEND_FUNCTION(rpmvercmp); +#if defined(HAVE_ARCHIVE) +ZEND_FUNCTION(rpmgetsymlink); +#endif + + +static const zend_function_entry ext_functions[] = { + ZEND_FE(rpmaddtag, arginfo_rpmaddtag) + ZEND_FE(rpmdbinfo, arginfo_rpmdbinfo) + ZEND_FE(rpmdbsearch, arginfo_rpmdbsearch) + ZEND_FE(rpminfo, arginfo_rpminfo) + ZEND_FE(rpmvercmp, arginfo_rpmvercmp) +#if defined(HAVE_ARCHIVE) + ZEND_FE(rpmgetsymlink, arginfo_rpmgetsymlink) +#endif + ZEND_FE_END +}; diff --git a/tests/002-rpmvercmp.phpt b/tests/002-rpmvercmp.phpt index d83cf67..c53a183 100644 --- a/tests/002-rpmvercmp.phpt +++ b/tests/002-rpmvercmp.phpt @@ -29,11 +29,53 @@ $ok = true; foreach ($cases as $case) { list($a,$b,$expected) = $case; $result = rpmvercmp($a,$b); - if ($result != $expected) { + if ($result !== $expected) { $ok = false; printf("rpmvercmp(%s, %s) = %d when %d expected\n", $a, $b, $result, $expected); } } + +$cases = [ + ['1', '2', '>', false], + ['1', '2', 'gt', false], + ['1', '2', '>=', false], + ['1', '2', 'ge', false], + ['1', '1', '>=', true], + ['1', '1', 'ge', true], + + ['1', '2', '<', true], + ['1', '2', 'lt', true], + ['1', '2', '<=', true], + ['1', '2', 'le', true], + ['1', '1', '<=', true], + ['1', '1', 'le', true], + + ['1', '1', '=', true], + ['1', '1', '==', true], + ['1', '1', 'eq', true], + + ['1', '2', '=', false], + ['1', '2', '==', false], + ['1', '2', 'eq', false], + + ['1', '1', '!=', false], + ['1', '1', '<>', false], + ['1', '1', 'ne', false], + + ['1', '2', '!=', true], + ['1', '2', '<>', true], + ['1', '2', 'ne', true], +]; +foreach ($cases as $case) { + list($a,$b,$op,$expected) = $case; + $result = rpmvercmp($a,$b,$op); + if ($result !== $expected) { + $ok = false; + printf("rpmvercmp(%s, %s, %s) = %s when %s expected\n", + $a, $b, $op, $result ? "true" : "false", $expected ? "true" : "false"); + } +} + if ($ok) echo "OK\n"; ?> Done diff --git a/tests/003-rpminfo.phpt b/tests/003-rpminfo.phpt index beb7b47..919bcc9 100644 --- a/tests/003-rpminfo.phpt +++ b/tests/003-rpminfo.phpt @@ -14,7 +14,7 @@ array(5) { ["Version"]=> string(1) "1" ["Release"]=> - string(11) "1.fc27.remi" + string(1) "3" ["Summary"]=> string(5) "Bidon" ["Arch"]=> diff --git a/tests/005-rpminfo-full.phpt b/tests/005-rpminfo-full.phpt index 38f5bab..15c158d 100644 --- a/tests/005-rpminfo-full.phpt +++ b/tests/005-rpminfo-full.phpt @@ -24,9 +24,11 @@ Done --- bidon.rpm --- string(5) "bidon" string(15) "A dummy package" -array(1) { +array(2) { [0]=> - string(8) "- create" + string(14) "- add symlinks" + [1]=> + string(20) "- add some hardlinks" } bool(false) array(1) { diff --git a/tests/010-rpmvercmp_error7.phpt b/tests/010-rpmvercmp_error7.phpt deleted file mode 100644 index c9b3875..0000000 --- a/tests/010-rpmvercmp_error7.phpt +++ /dev/null @@ -1,25 +0,0 @@ ---TEST-- -Check for rpmvercmp function error ---SKIPIF-- -<?php -if (!extension_loaded("rpminfo")) print "skip"; -if (PHP_VERSION_ID >= 80000) print "skip only for PHP 7"; -?> ---FILE-- -<?php -var_dump(rpmvercmp()); -var_dump(rpmvercmp("a")); -var_dump(rpmvercmp("a", "b", "c")); -?> -Done ---EXPECTF-- - -Warning: rpmvercmp() expects exactly 2 parameters, 0 given in %s -NULL - -Warning: rpmvercmp() expects exactly 2 parameters, 1 given in %s -NULL - -Warning: rpmvercmp() expects exactly 2 parameters, 3 given in %s -NULL -Done diff --git a/tests/011-rpmvercmp_error8.phpt b/tests/011-rpmvercmp_error8.phpt index 4430975..2fe6aa1 100644 --- a/tests/011-rpmvercmp_error8.phpt +++ b/tests/011-rpmvercmp_error8.phpt @@ -19,13 +19,19 @@ try { } try { var_dump(rpmvercmp("a", "b", "c")); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +try { + var_dump(rpmvercmp("a", "b", "c", "d")); } catch (ArgumentCountError $e) { echo $e->getMessage(), "\n"; } ?> Done --EXPECTF-- -rpmvercmp() expects exactly 2 %s, 0 given -rpmvercmp() expects exactly 2 %s, 1 given -rpmvercmp() expects exactly 2 %s, 3 given +rpmvercmp() expects at least 2 %s, 0 given +rpmvercmp() expects at least 2 %s, 1 given +rpmvercmp(): Argument #3 ($operator) must be a valid comparison operator +rpmvercmp() expects at most 3 %s, 4 given Done diff --git a/tests/012-rpmaddtag.phpt b/tests/012-rpmaddtag.phpt new file mode 100644 index 0000000..bf371a4 --- /dev/null +++ b/tests/012-rpmaddtag.phpt @@ -0,0 +1,16 @@ +--TEST-- +Check for rpmaddtag parameter check +--SKIPIF-- +<?php if (!extension_loaded("rpminfo")) print "skip"; ?> +--FILE-- +<?php +var_dump(rpmaddtag(RPMTAG_INSTALLTIME)); +try { + var_dump(rpmaddtag(-1)); +} catch (ValueError $e) { + echo $e->getMessage(); +} +?> +--EXPECTF-- +bool(true) +%A Unkown rpmtag%A diff --git a/tests/013-rpmdbsearch-error.phpt b/tests/013-rpmdbsearch-error.phpt new file mode 100644 index 0000000..4a61227 --- /dev/null +++ b/tests/013-rpmdbsearch-error.phpt @@ -0,0 +1,16 @@ +--TEST-- +Check for rpmdbinfo function +--SKIPIF-- +<?php if (!extension_loaded("rpminfo")) print "skip"; ?> +--FILE-- +<?php +var_dump(rpmdbsearch('notexists', RPMTAG_NAME)); +try { +var_dump(rpmdbsearch('notexists', RPMTAG_NAME, 99)); +} catch (ValueError $e) { + echo $e->getMessage(); +} +?> +--EXPECTF-- +NULL +%A Unkown rpmmire%A diff --git a/tests/014-stream.phpt b/tests/014-stream.phpt new file mode 100644 index 0000000..69b881e --- /dev/null +++ b/tests/014-stream.phpt @@ -0,0 +1,97 @@ +--TEST-- +Check for stream +--SKIPIF-- +<?php +if (!extension_loaded("rpminfo")) print "skip"; +if (version_compare(RPMVERSION, '4.13', 'lt')) print("skip librpm is older than 4.13"); +?> +--FILE-- +<?php +$d = "rpm://" . __DIR__ . "/bidon.rpm#/usr/share/doc/bidon"; +$n = "rpm://" . __DIR__ . "/bidon.rpm#/usr/share/doc/bidon/README"; +$x = "rpm://" . __DIR__ . "/bidon.rpm#/usr/share/doc/bidon/MISSING"; +$foo = "rpm://" . __DIR__ . "/bidon.rpm#/etc/foo.conf"; +$bar = "rpm://" . __DIR__ . "/bidon.rpm#/etc/bar.conf"; +$toto = "rpm://" . __DIR__ . "/bidon.rpm#/etc/toto.conf"; + +echo "+ wrapper\n"; +var_dump(in_array('rpm', stream_get_wrappers())); + +echo "+ stat\n"; +$s = stat($d); // S_ISDIR +var_dump($s['size'], $s['mode'] , ($s['mode'] & 0170000) == 0040000 ? "OK" : "KO"); +var_dump(file_exists($d), is_dir($d), is_file($d), is_link($n)); +$s = stat($n); // S_ISREG +var_dump($s['size'], $s['mode'] , ($s['mode'] & 0170000) == 0100000 ? "OK" : "KO"); +var_dump(file_exists($n), is_dir($n), is_file($n), is_link($n)); +$s = stat($toto); // S_ISLNK +var_dump($s['size'], $s['mode'] , ($s['mode'] & 0170000) == 0120000 ? "OK" : "KO"); +var_dump(file_exists($toto), is_dir($toto), is_file($toto), is_link($toto)); + +echo "+ file\n"; +var_dump($f = fopen($n, "r")); +$s = fstat($f); +var_dump($s['size'], $s['mode']); +var_dump(trim(fread($f, 10))); +var_dump(feof($f)); +var_dump(trim(fread($f, 100))); +var_dump(feof($f)); +fclose($f); + +echo "+ stream\n"; +var_dump(trim(file_get_contents($n))); // Existing file +var_dump(trim(file_get_contents($foo))); // Hardlink with content +var_dump(trim(file_get_contents($bar))); // hardlink without content +var_dump(file_get_contents($x)); // Missing file + +echo "+ symlink\n"; +var_dump(rpmgetsymlink(__DIR__ . "/bidon.rpm", "missing")); +var_dump(rpmgetsymlink(__DIR__ . "/bidon.rpm", "/etc/foo.conf")); // not a symlink +var_dump(rpmgetsymlink(__DIR__ . "/bidon.rpm", "/etc/toto.conf")); // symlink +?> +Done +--EXPECTF-- ++ wrapper +bool(true) ++ stat +int(0) +int(16877) +string(2) "OK" +bool(true) +bool(true) +bool(false) +bool(false) +int(30) +int(33188) +string(2) "OK" +bool(true) +bool(false) +bool(true) +bool(false) +int(8) +int(41471) +string(2) "OK" +bool(true) +bool(false) +bool(false) +bool(true) ++ file +resource(%d) of type (stream) +int(30) +int(33188) +string(10) "Thu Oct 19" +bool(false) +string(18) "12:01:02 CEST 2023" +bool(true) ++ stream +string(29) "Thu Oct 19 12:01:02 CEST 2023" +string(7) "content" +string(7) "content" + +Warning: file_get_contents(%s/bidon.rpm#/usr/share/doc/bidon/MISSING): Failed to open stream: operation failed in %s on line %d +bool(false) ++ symlink +NULL +string(0) "" +string(8) "foo.conf" +Done diff --git a/tests/bidon.rpm b/tests/bidon.rpm Binary files differindex 6cbfa04..d8efb06 100644 --- a/tests/bidon.rpm +++ b/tests/bidon.rpm diff --git a/tests/bidon.spec b/tests/bidon.spec new file mode 100644 index 0000000..b3ad790 --- /dev/null +++ b/tests/bidon.spec @@ -0,0 +1,42 @@ +%{!?ver: %global ver 1} + +Name: bidon +Version: %{ver} +Release: 3 +Summary: Bidon +License: Public Domain +URL: https://rpms.remirepo.net/ + +Obsoletes: fooobs < 2 + + +%description +A dummy package + +%prep +date >README +echo "content" >conf + +%build +: nothing to build + +%install +install -Dpm644 conf %{buildroot}%{_sysconfdir}/foo.conf +cd %{buildroot}%{_sysconfdir} +ln foo.conf bar.conf +ln -s foo.conf toto.conf + +%files +%doc README +%config(noreplace) %{_sysconfdir}/*.conf + + +%changelog +* Thu Oct 19 2023 Remi Collet <remi@fedoraproject.org> - 1-3 +- add symlinks + +* Fri Oct 13 2023 Remi Collet <remi@fedoraproject.org> - 1-2 +- add some hardlinks + +* Wed Dec 24 2014 Remi Collet <remi@fedoraproject.org> - 1-1 +- create |