[packages/poldek] Up to 0.46.0 git snapshot 02e02fb9 with trurlib c8c3443 and tndb 80e426f

arekm arekm at pld-linux.org
Fri Oct 23 18:30:15 CEST 2026


commit 4aeb92834357540e7a0f8b9e4a72effdb289c3a0
Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
Date:   Fri Sep 18 23:46:40 2026 +0200

    Up to 0.46.0 git snapshot 02e02fb9 with trurlib c8c3443 and tndb 80e426f
    
    Drop patches merged upstream.

 poldek-config.patch                     |  11 +-
 poldek-dup-sources.patch                | 143 --------
 poldek-env-columns-lines.patch          |  65 ----
 poldek-ftp-pasv-crash.patch             |  47 ---
 poldek-ftp-pasv-parse.patch             |  19 -
 poldek-global-ignore-merges.patch       | 193 ----------
 poldek-multilib-bare-name-install.patch | 247 -------------
 poldek-nocolor-cmp.patch                |  56 ---
 poldek-scoring-evr.patch                | 101 ------
 poldek-search-i-opt.patch               | 138 --------
 poldek-url-keep-port.patch              |  50 ---
 poldek-vcn-pool-reuse.patch             |  53 ---
 poldek-vfile-net-tests.patch            | 603 --------------------------------
 poldek-vfstat-redirect-crash.patch      |  27 --
 poldek.spec                             | 107 +++---
 15 files changed, 59 insertions(+), 1801 deletions(-)
---
diff --git a/poldek.spec b/poldek.spec
index ce36d8b..9d2d1e3 100644
--- a/poldek.spec
+++ b/poldek.spec
@@ -22,18 +22,22 @@
 %define		ver_rpm		1:4.14
 %endif
 
-%define		rel	12
+%define		gitref	02e02fb938d1e4c8228012d4d1f1c90aa7d26832
+%define		snap	20261023
+%define		rel	0.1
+# poldek's autogen.sh and CI build against trurlib and tndb master
+%define		trurlib_gitref	c8c3443df37a175f68ac9f0c55978dbb6d238c33
+%define		tndb_gitref	80e426fe181798cc2780e6ca18f30ad8b69297b0
 Summary:	RPM packages management helper tool
 Summary(hu.UTF-8):	RPM csomagkezelést segítő eszköz
 Summary(pl.UTF-8):	Pomocnicze narzędzie do zarządzania pakietami RPM
 Name:		poldek
-Version:	0.45.0
-Release:	%{rel}%{?with_snap:.%{SNAP}}
+Version:	0.46.0
+Release:	0.%{snap}.%{rel}%{?with_snap:.%{SNAP}}
 License:	GPL v2
 Group:		Applications/System
-#Source0:	http://poldek.pld-linux.org/download/snapshots/%{name}-%{version}-cvs%{snap}.tar.bz2
-Source0:	https://github.com/poldek-pm/poldek/releases/download/v%{version}/%{name}-%{version}.tar.xz
-# Source0-md5:	445033ef4d1312718d27b4b7fc197643
+Source0:	https://github.com/poldek-pm/poldek/archive/%{gitref}/%{name}-%{snap}.tar.gz
+# Source0-md5:	d5b585755b7683948894f378a7345ea6
 Source1:	%{name}.conf
 Source2:	%{name}-multilib.conf
 Source3:	%{name}-config.sh
@@ -45,25 +49,16 @@ Source8:	%{name}-debuginfo.conf
 Source9:	%{name}-aidath.conf
 Source10:	%{name}-multilib-aidath.conf
 Source11:	%{name}-archive.conf
+Source12:	https://github.com/poldek-pm/trurlib/archive/%{trurlib_gitref}/trurlib-%{trurlib_gitref}.tar.gz
+# Source12-md5:	bc8baff5e9c04e59e468b1284e2ae341
+Source13:	https://github.com/poldek-pm/tndb/archive/%{tndb_gitref}/tndb-%{tndb_gitref}.tar.gz
+# Source13-md5:	250fc79aed3258dfa6c336a40d571978
 Source100:	%{name}-snap.conf
 Source101:	%{name}-multilib-snap.conf
 Source102:	%{name}-debuginfo-snap.conf
 Patch0:		%{name}-config.patch
 Patch1:		pm-hooks.patch
 Patch2:		%{name}-ext-down-enable.patch
-Patch3:		%{name}-search-i-opt.patch
-Patch4:		%{name}-nocolor-cmp.patch
-Patch5:		%{name}-multilib-bare-name-install.patch
-Patch6:		%{name}-dup-sources.patch
-Patch7:		%{name}-env-columns-lines.patch
-Patch8:		%{name}-scoring-evr.patch
-Patch9:		%{name}-global-ignore-merges.patch
-Patch10:	%{name}-ftp-pasv-crash.patch
-Patch11:	%{name}-ftp-pasv-parse.patch
-Patch12:	%{name}-vfstat-redirect-crash.patch
-Patch13:	%{name}-vcn-pool-reuse.patch
-Patch14:	%{name}-url-keep-port.patch
-Patch15:	%{name}-vfile-net-tests.patch
 URL:		http://poldek.pld-linux.org/
 %{?with_rpm5:BuildRequires:	%{db_pkg}-devel >= %{ver_db}}
 BuildRequires:	autoconf >= 2.63
@@ -72,10 +67,13 @@ BuildRequires:	bzip2-devel
 BuildRequires:	check-devel
 BuildRequires:	docbook-dtd412-xml
 BuildRequires:	docbook2X
+BuildRequires:	gettext-autopoint
 BuildRequires:	gettext-tools >= 0.11.5
 BuildRequires:	libgomp-devel
 BuildRequires:	libtool
 BuildRequires:	libxml2-devel >= 2
+BuildRequires:	libxml2-progs
+BuildRequires:	libxslt-progs
 BuildRequires:	lua54-devel
 BuildRequires:	ncurses-devel
 BuildRequires:	openssl-devel >= 0.9.7d
@@ -235,33 +233,31 @@ Python modulok poldek-hez.
 Moduły języka Python dla poldka.
 
 %prep
-%setup -q
+%setup -q -n %{name}-%{gitref} -a12 -a13
+rmdir trurlib tndb
+%{__mv} trurlib-%{trurlib_gitref} trurlib
+%{__mv} tndb-%{tndb_gitref} tndb
+
+# conf/*.conf are generated from doc/poldek.conf.xml and make regenerates them
+# when the xml is newer, so generate them after any patch to the xml and apply
+# the PLD defaults (Patch0-2) to the result
+cd conf
+for c in fetch source poldek; do
+	%{__sh} ../doc/conf-xml2.sh conf ../doc/poldek.conf.xml $c
+done
+cd ..
 %patch -P0 -p1
 %patch -P1 -p1
 %patch -P2 -p1
-%patch -P3 -p1
-%patch -P4 -p1
-%patch -P5 -p1
-%patch -P6 -p1
-%patch -P7 -p1
-%patch -P8 -p1
-%patch -P9 -p1
-%patch -P10 -p1
-%patch -P11 -p1
-%patch -P12 -p1
-%patch -P13 -p1
-%patch -P14 -p1
-%patch -P15 -p1
-
-%{__rm} doc/poldek.info
-%{__rm} m4/libtool.m4 m4/lt*.m4
+
+# what upstream autogen.sh does besides autotools
+%{__make} -f Makefile.extra POTFILES_in
 
 # cleanup backups after patching
 find . '(' -name '*~' -o -name '*.orig' ')' -print0 | xargs -0 -r -l512 rm -f
-chmod u+x ./configure ./doc/conf-xml2.sh
 
 %build
-%{__gettextize}
+%{__autopoint}
 %{__libtoolize}
 %{__aclocal} -I m4
 %{__autoheader}
@@ -289,7 +285,7 @@ cd ..
 	%{?with_python:--with-python}
 %{__make}
 
-%{__make} -C doc poldek.info
+%{__make} -C doc manual.html poldek.info
 
 %if %{with python}
 %{__make} -C python
@@ -433,6 +429,7 @@ cp -p %{SOURCE7} $RPM_BUILD_ROOT%{_pixmapsdir}/%{name}.png
 %{__rm} -rf configs
 cp -a conf configs
 %{__rm} -f configs/Makefile*
+%{__rm} configs/.gitignore
 
 %if %{with python}
 %py_postclean
@@ -545,26 +542,26 @@ fi
 %if %{without static}
 %files libs
 %defattr(644,root,root,755)
-%attr(755,root,root) %{_libdir}/libpoclidek.so.*.*.*
-%attr(755,root,root) %{_libdir}/libpoldek.so.*.*.*
-%attr(755,root,root) %{_libdir}/libtndb.so.*.*.*
-%attr(755,root,root) %{_libdir}/libtrurl.so.*.*.*
-%attr(755,root,root) %{_libdir}/libvfile.so.*.*.*
-%attr(755,root,root) %ghost %{_libdir}/libpoclidek.so.2
-%attr(755,root,root) %ghost %{_libdir}/libpoldek.so.4
-%attr(755,root,root) %ghost %{_libdir}/libtndb.so.0
-%attr(755,root,root) %ghost %{_libdir}/libtrurl.so.10
-%attr(755,root,root) %ghost %{_libdir}/libvfile.so.0
+%{_libdir}/libpoclidek.so.*.*.*
+%{_libdir}/libpoldek.so.*.*.*
+%{_libdir}/libtndb.so.*.*.*
+%{_libdir}/libtrurl.so.*.*.*
+%{_libdir}/libvfile.so.*.*.*
+%ghost %{_libdir}/libpoclidek.so.2
+%ghost %{_libdir}/libpoldek.so.4
+%ghost %{_libdir}/libtndb.so.0
+%ghost %{_libdir}/libtrurl.so.10
+%ghost %{_libdir}/libvfile.so.0
 %endif
 
 %files devel
 %defattr(644,root,root,755)
 %if %{without static}
-%attr(755,root,root) %{_libdir}/libpoclidek.so
-%attr(755,root,root) %{_libdir}/libpoldek.so
-%attr(755,root,root) %{_libdir}/libtndb.so
-%attr(755,root,root) %{_libdir}/libtrurl.so
-%attr(755,root,root) %{_libdir}/libvfile.so
+%{_libdir}/libpoclidek.so
+%{_libdir}/libpoldek.so
+%{_libdir}/libtndb.so
+%{_libdir}/libtrurl.so
+%{_libdir}/libvfile.so
 %endif
 %{_libdir}/libpoclidek.la
 %{_libdir}/libpoldek.la
@@ -586,7 +583,7 @@ fi
 %if %{with python}
 %files -n python-poldek
 %defattr(644,root,root,755)
-%attr(755,root,root) %{py_sitedir}/_poldekmod.so
+%{py_sitedir}/_poldekmod.so
 %{py_sitescriptdir}/poldek.py[co]
 %{py_sitescriptdir}/poldekmod.py[co]
 %endif
diff --git a/poldek-config.patch b/poldek-config.patch
index f89f304..05e13a8 100644
--- a/poldek-config.patch
+++ b/poldek-config.patch
@@ -1,13 +1,16 @@
---- poldek-0.30.0/conf/poldek.conf	2013-07-08 14:19:35.294351501 +0300
-+++ poldek-0.30.0/conf/poldek.conf	2013-07-08 14:06:38.722878665 +0300
-@@ -132,12 +132,11 @@
+--- a/conf/poldek.conf
++++ b/conf/poldek.conf
+@@ -135,15 +135,14 @@
  
  # Prevent package listed from being upgraded if they are already installed.
  # hold = kernel* foo*.i686 th-test:* *-smp-* th-ready:bar*.x86_64
 -#hold = 
 +hold = kernel*
  
- # Ignore package list - packages fits given mask will be invisible.
+ # Ignore package list - packages matching given masks will be invisible.
+ # Patterns listed here apply to every source; any
+ # per-source ignore entry adds more masks on top of this
+ # global list rather than replacing it.
  # ignore = *-smp-* foo*
 -# ignore = vserver-packages
 -#ignore = vserver-packages
diff --git a/poldek-dup-sources.patch b/poldek-dup-sources.patch
deleted file mode 100644
index f98da4a..0000000
--- a/poldek-dup-sources.patch
+++ /dev/null
@@ -1,143 +0,0 @@
-commit bd97788fa990b4b2cd957c540399cec35afab526
-Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
-Date:   Tue Apr 14 16:26:11 2026 +0200
-
-    Show config file origin when removing duplicated sources
-    
-    Print which config file:line each duplicated source was defined at,
-    making it easy to find and fix the duplication.
-    
-    Also fix conf.c filemark formatting: remove trailing colon from
-    stored __file__line value and format strings to avoid double-colon
-    artifacts in error messages.
-
-diff --git a/conf.c b/conf.c
-index 83e625f6..f9f775b0 100644
---- a/conf.c
-+++ b/conf.c
-@@ -505,7 +505,7 @@ static int verify_param_presence(tn_hash *ht_sect, const char *section,
- 
-     if (overwrite || (tag->flags & CONF_TYPE_F_MULTI_EXCL)) {
-         if (!overwrite || poldek_VERBOSE > 1)
--            logn(LOGWARN, _("%s %s::%s redefined"), filemark, section, name);
-+            logn(LOGWARN, _("%s: %s::%s redefined"), filemark, section, name);
-         n_hash_remove(ht_sect, name);
- 
-     } else if ((tag->flags & CONF_TYPE_F_MULTI) == 0) {
-@@ -541,9 +541,9 @@ static int add_param(tn_hash *ht_sect, const char *section,
-     validate = (flags & ADD_PARAM_VALIDATE);
- 
-     if (path)
--        n_snprintf(filemark, sizeof(filemark), "%s:%d:", path, nline);
-+        n_snprintf(filemark, sizeof(filemark), "%s:%d", path, nline);
-     else
--        n_snprintf(filemark, sizeof(filemark), "config:");
-+        n_snprintf(filemark, sizeof(filemark), "config");
- 
-     if ((tagindex = find_tag(section, name, &sect)) == -1) {
-         if (*name == '_')       /* internal or _macro */
-@@ -554,7 +554,7 @@ static int add_param(tn_hash *ht_sect, const char *section,
-             tag = &unknown_tag;
- 
-         } else {
--            logn(LOGWARN, _("%s unknown option '%s::%s'"), filemark,
-+            logn(LOGWARN, _("%s: unknown option '%s::%s'"), filemark,
-                  section, name);
-             return 0;
-         }
-@@ -604,7 +604,7 @@ static int add_param(tn_hash *ht_sect, const char *section,
-         val = "";
- 
-     if (val == NULL) {
--        logn(LOGERR, _("%s invalid value of '%s::%s'"), filemark, section, name);
-+        logn(LOGERR, _("%s: invalid value of '%s::%s'"), filemark, section, name);
-         return 0;
-     }
- 
-@@ -618,7 +618,7 @@ static int add_param(tn_hash *ht_sect, const char *section,
-         }
- 
-         if (!valid) {
--            logn(LOGWARN, _("%s invalid value '%s' of '%s::%s'"), filemark,
-+            logn(LOGWARN, _("%s: invalid value '%s' of '%s::%s'"), filemark,
-                  val, section, name);
-             return 0;
-         }
-@@ -826,7 +826,7 @@ static tn_hash *open_section_ht(tn_hash *htconf,
-         char filemark[PATH_MAX];
-         struct copt *opt;
- 
--        n_snprintf(filemark, sizeof(filemark), "%s:%d:", path, nline);
-+        n_snprintf(filemark, sizeof(filemark), "%s:%d", path, nline);
-         opt = copt_new("__file__line");
-         opt->val = n_strdup(filemark);
-         n_hash_insert(ht_sect, opt->name, opt);
-diff --git a/pkgdir/source.c b/pkgdir/source.c
-index 28c0049f..8d23592d 100644
---- a/pkgdir/source.c
-+++ b/pkgdir/source.c
-@@ -176,6 +176,7 @@ struct source *source_malloc(void)
-     //src->flags |= PKGSOURCE_PRI;
-     src->name = src->path = src->pkg_prefix = NULL;
-     src->group = src->dscr = NULL;
-+    src->config_origin = NULL;
-     src->lc_lang = NULL;
-     src->_refcnt = 0;
-     src->exclude_path = n_array_new(4, free, (tn_fn_cmp)strcmp);
-@@ -213,6 +214,7 @@ struct source *source_clone(const struct source *src)
-     cp_str_ifnotnull(&nsrc->group, src->group);
-     cp_str_ifnotnull(&nsrc->lc_lang, src->lc_lang);
-     cp_str_ifnotnull(&nsrc->original_type, src->original_type);
-+    cp_str_ifnotnull(&nsrc->config_origin, src->config_origin);
- 
-     n_array_free(nsrc->exclude_path);
-     nsrc->exclude_path = n_ref(src->exclude_path);
-@@ -241,6 +243,7 @@ void source_free(struct source *src)
-     n_cfree(&src->group);
-     n_cfree(&src->lc_lang);
-     n_cfree(&src->original_type);
-+    n_cfree(&src->config_origin);
- 
-     if (src->exclude_path)
-         n_array_free(src->exclude_path);
-@@ -662,6 +665,10 @@ struct source *source_new_htcnf(const tn_hash *htcnf)
-     if (vs)
-         src->original_type = n_strdup(vs);
- 
-+    vs = poldek_conf_get(htcnf, "__file__line", NULL);
-+    if (vs)
-+        src->config_origin = n_strdup(vs);
-+
-     get_conf_opt_list(htcnf, "exclude path", src->exclude_path);
-     get_conf_opt_list(htcnf, "ignore", src->ign_patterns);
-     return src;
-@@ -687,11 +694,16 @@ int source_cmp_uniq(const struct source *s1, const struct source *s2)
-         rc = strcmp(n1, n2);
-     }
- 
--    if (rc == 0)
-+    if (rc == 0) {
-         logn(LOGWARN, _("removed duplicated source %s%s%s"),
-              (s2->flags & PKGSOURCE_NAMED) ? s2->name : "",
-              (s2->flags & PKGSOURCE_NAMED) ? " -- " : "",
-              s2->path);
-+        if (s1->config_origin || s2->config_origin)
-+            logn(LOGWARN, _("  defined at %s and %s"),
-+                 s1->config_origin ? s1->config_origin : "(unknown)",
-+                 s2->config_origin ? s2->config_origin : "(unknown)");
-+    }
- 
-     return rc;
- }
-diff --git a/pkgdir/source.h b/pkgdir/source.h
-index 6052ec25..b93639b5 100644
---- a/pkgdir/source.h
-+++ b/pkgdir/source.h
-@@ -55,6 +55,7 @@ struct source {
-     tn_array  *exclude_path;
-     tn_array  *ign_patterns;    /* ignore package patterns */
-     char      *original_type;   /* type of source repo for this source  */
-+    char      *config_origin;   /* config file:line where source was defined */
-     unsigned  subopt_flags;
-     int       _refcnt;
-     char      *group;
diff --git a/poldek-env-columns-lines.patch b/poldek-env-columns-lines.patch
deleted file mode 100644
index 89198e5..0000000
--- a/poldek-env-columns-lines.patch
+++ /dev/null
@@ -1,65 +0,0 @@
-commit bcf31cc1653155c93ad4b2b1a17d2301ac70d36b
-Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
-Date:   Tue Apr 14 17:45:51 2026 +0200
-
-    Use $COLUMNS as fallback when ioctl fails to get terminal width
-    
-    When stdout is not a tty (e.g. piped output), ioctl TIOCGWINSZ fails
-    and terminal width falls back to 80 columns, causing unnecessary URL
-    truncation. Check $COLUMNS environment variable before falling back
-    to the default.
-
-diff --git a/poldek_term.c b/poldek_term.c
-index 9d4f9a87..6fbf7d82 100644
---- a/poldek_term.c
-+++ b/poldek_term.c
-@@ -245,15 +245,34 @@ static void update_term_width(void)
- 
-     if (winch_reached) {
-         char tmp[256];
-+        term_width  = TERM_DEFAULT_WIDTH;
-+        term_height = TERM_DEFAULT_HEIGHT;
-+
-         if (ioctl(1, TIOCGWINSZ, &ws) == 0) {
-             term_width  = ws.ws_col;
-             term_height = ws.ws_row;
- 
-         } else {
--            term_width  = TERM_DEFAULT_WIDTH;
--            term_height = TERM_DEFAULT_HEIGHT;
-+            const char *cols = getenv("COLUMNS");
-+            if (cols) {
-+                int n = atoi(cols);
-+                if (n > 0)
-+                    term_width = n;
-+            }
-+
-+            const char *rows = getenv("LINES");
-+            if (rows) {
-+                int n = atoi(rows);
-+                if (n > 0)
-+                    term_height = n;
-+            }
-         }
- 
-+        if (term_width < TERM_MIN_WIDTH)
-+            term_width = TERM_MIN_WIDTH;
-+        if (term_height < TERM_MIN_HEIGHT)
-+            term_height = TERM_MIN_HEIGHT;
-+
-         //https://www.gnu.org/software/libc/manual/html_node/Argp-User-Customization.html
-         snprintf(tmp, sizeof(tmp), "no-dup-args-note,rmargin=%d", term_width - 1);
-         setenv("ARGP_HELP_FMT", tmp, 1);
-diff --git a/poldek_term.h b/poldek_term.h
-index 80e6d5a6..2b1db740 100644
---- a/poldek_term.h
-+++ b/poldek_term.h
-@@ -25,6 +25,8 @@
- 
- #define TERM_DEFAULT_WIDTH  80
- #define TERM_DEFAULT_HEIGHT 24
-+#define TERM_MIN_WIDTH      40
-+#define TERM_MIN_HEIGHT      4
- 
- #include <stddef.h>           /* for size_t     */
- 
diff --git a/poldek-ftp-pasv-crash.patch b/poldek-ftp-pasv-crash.patch
deleted file mode 100644
index 14f8c0d..0000000
--- a/poldek-ftp-pasv-crash.patch
+++ /dev/null
@@ -1,47 +0,0 @@
-    fix: do not crash when the FTP PASV reply cannot be read
-
-    vftpcn_pasv() handed the server's response text to vfff_set_err() as the
-    printf format string.  When the reply cannot be read at all - a timeout
-    or a reset control connection, which is exactly what a flaky link
-    produces - vftpcn_resp() fails and resp_msg() is NULL, so vsnprintf()
-    gets a NULL format and the process dies with SIGSEGV.  A server that
-    answers PASV with a message containing conversions is a format string
-    bug on its own.
-
-    Bail out on an unreadable response and keep the error readresp() has
-    already set instead of overwriting it with a bare EIO, and print a
-    non-conforming reply through "%s".
-
-    Annotate vfff_set_err() as printf-like so -Wformat-security, already a
-    build error here, rejects the next such call at compile time.
-
-diff --git a/vfile/vfff/ftp.c b/vfile/vfff/ftp.c
---- a/vfile/vfff/ftp.c
-+++ b/vfile/vfff/ftp.c
-@@ -556,8 +556,11 @@
-     if (!vftpcn_cmd(cn, cmd))
-         return 0;
- 
--    if (!vftpcn_resp(cn) || resp_code(cn) != req_code) {
--        vfff_set_err(EIO, resp_msg(cn));
-+    if (!vftpcn_resp(cn))       /* no response at all; readresp() set the cause */
-+        return 0;
-+
-+    if (resp_code(cn) != req_code) {
-+        vfff_set_err(EIO, "%s", resp_msg(cn));
-         return 0;
-     }
- 
-diff --git a/vfile/vfff/vfff.h b/vfile/vfff/vfff.h
---- a/vfile/vfff/vfff.h
-+++ b/vfile/vfff/vfff.h
-@@ -39,7 +39,8 @@
- extern void (*vfff_vlog_cb)(const char *fmt, va_list ap);
- 
- const char *vfff_errmsg(void);
--void vfff_set_err(int err_no, const char *fmt, ...);
-+void vfff_set_err(int err_no, const char *fmt, ...)
-+    __attribute__((format(printf, 2, 3)));
- void vfff_log(const char *fmt, ...);
- int vfff_sigint_reached(void);
- int vfff_to_connect(const char *host, const char *service, int *af);
diff --git a/poldek-ftp-pasv-parse.patch b/poldek-ftp-pasv-parse.patch
deleted file mode 100644
index c58209b..0000000
--- a/poldek-ftp-pasv-parse.patch
+++ /dev/null
@@ -1,19 +0,0 @@
-    fix: bound the PASV address scan
-
-    parse_pasv() skipped to the first digit with an unbounded loop, so a 227
-    reply whose text carries no digit at all reads past the end of the
-    response buffer.  Stopping at the terminator lets the sscanf() below
-    fail and report a parse error like any other malformed reply.
-
-diff --git a/vfile/vfff/ftp.c b/vfile/vfff/ftp.c
---- a/vfile/vfff/ftp.c
-+++ b/vfile/vfff/ftp.c
-@@ -499,7 +499,7 @@
-     is_err = 0;
- 
-     p = resp;
--    while (!isdigit(*p))
-+    while (*p && !isdigit(*p))
-         p++;
- 
-     if (sscanf(p, "%d,%d,%d,%d,%d,%d", &a[0], &a[1], &a[2], &a[3],
diff --git a/poldek-global-ignore-merges.patch b/poldek-global-ignore-merges.patch
deleted file mode 100644
index e7776a7..0000000
--- a/poldek-global-ignore-merges.patch
+++ /dev/null
@@ -1,193 +0,0 @@
-commit aea999f2f1cb86579bc8fef9f5393120d12f863b
-Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
-Date:   Wed Apr 22 09:50:32 2026 +0200
-
-    fix: apply [global] + per-source ignore across all pkg paths
-    
-    [global] ignore = ... was silently replaced by any per-source
-    ignore = ... entry rather than extended, so global patterns were
-    lost on every source with its own ignore line. And the stub-index
-    load path (poldek --cmd "ls", search, etc.) bypassed ignore
-    filtering entirely for PKGDIR_CAP_HANDLEIGNORE modules -- that cap
-    only covers the full do_load path, not the separate stub .zst
-    cache loaded by load_stubindex.
-    
-    - lib_init.c: always concat [global] ign_patterns into src's.
-    - lib_pkgset.c: in poldek_load_stubs, post-filter per-source stubs
-      via src->ign_patterns, symmetric with the full-load path and
-      gated by POLDEK_OP_IGNORE.
-    - pkgdir/pkgdir_stubindex.c: source_stubload is now a pure loader;
-      the HANDLEIGNORE guard there was dropping filtering entirely for
-      pndir sources in the stub path.
-
-diff --git a/doc/poldek.conf.xml b/doc/poldek.conf.xml
-index 86cb545b..828bb683 100644
---- a/doc/poldek.conf.xml
-+++ b/doc/poldek.conf.xml
-@@ -395,7 +395,10 @@ and [filename]*-source.conf[/filename], file getters are declared in
- 
-   <option name="ignore" type="string" list="yes" default="vserver-packages" multiple="yes">
-     <description>
--    Ignore package list - packages fits given mask will be invisible.
-+    Ignore package list - packages matching given masks will be invisible.
-+    Patterns listed here apply to every [option]source[/option]; any
-+    per-source [option]ignore[/option] entry adds more masks on top of this
-+    global list rather than replacing it.
-     [screen]
-  ignore = *-smp-* foo*
-  ignore = vserver-packages
-@@ -634,7 +637,9 @@ Every repository is configured in its own [ source ] section.
- 
-   <option name="ignore" type="string" list="yes" default="" multiple="yes">
-     <description>
--    Have the same meaning as [ global ] parameter. Example:
-+    Additional ignore masks for this source, merged with the [ global ]
-+    [option]ignore[/option] list (global patterns always apply; per-source
-+    entries add more). Example:
-     [screen]
-  ignore = kernel*smp* dev
-     [/screen]
-diff --git a/lib_init.c b/lib_init.c
-index 297ec37e..88684654 100644
---- a/lib_init.c
-+++ b/lib_init.c
-@@ -245,12 +245,11 @@ struct source *do_source_new_htcnf(struct poldek_ctx *ctx,
-                                         (tn_fn_dup)strdup);
-     }
- 
--    if (n_array_size(src->ign_patterns) == 0 && /* take global  */
--        n_array_size(ctx->ts->ign_patterns) > 0) {
--
--        n_array_free(src->ign_patterns);
--        src->ign_patterns = n_array_dup(ctx->ts->ign_patterns,
--                                        (tn_fn_dup)strdup);
-+    if (n_array_size(ctx->ts->ign_patterns) > 0) {
-+        n_array_concat_ex(src->ign_patterns, ctx->ts->ign_patterns,
-+                          (tn_fn_dup)strdup);
-+        n_array_sort(src->ign_patterns);
-+        n_array_uniq(src->ign_patterns);
-     }
- 
-     return src;
-diff --git a/lib_pkgset.c b/lib_pkgset.c
-index 1b19752e..0b676fc9 100644
---- a/lib_pkgset.c
-+++ b/lib_pkgset.c
-@@ -129,6 +129,7 @@ tn_array *poldek_load_stubs(struct poldek_ctx *ctx)
- 
-     n_array_isort_ex(sources, (tn_fn_cmp)source_cmp_pri);
-     tn_array *stubpkgs = pkgs_array_new(4096);
-+    int do_ignore = ctx->ts->getop(ctx->ts, POLDEK_OP_IGNORE);
- 
-     for (i=0; i < n_array_size(sources); i++) {
-         struct source *src = n_array_nth(sources, i);
-@@ -145,6 +146,9 @@ tn_array *poldek_load_stubs(struct poldek_ctx *ctx)
-             return 0;
-         }
- 
-+        if (do_ignore && n_array_size(src->ign_patterns) > 0)
-+            packages_score_ignore(pkgs, src->ign_patterns, 1);
-+
-         while (n_array_size(pkgs) > 0) {
-             struct pkg *pkg = n_array_shift(pkgs);
- 
-diff --git a/pkgdir/pkgdir_stubindex.c b/pkgdir/pkgdir_stubindex.c
-index 7707378b..a7532896 100644
---- a/pkgdir/pkgdir_stubindex.c
-+++ b/pkgdir/pkgdir_stubindex.c
-@@ -204,13 +204,6 @@ tn_array *source_stubload(struct source *src)
-     if (pkgs == NULL)
-         return NULL;
- 
--    if (src->ign_patterns) {
--        const struct pkgdir_module *mod = pkgdir_mod_find(src->type);
--        /* module does not handle "ignore" itself  */
--        if (mod && (mod->cap_flags & PKGDIR_CAP_HANDLEIGNORE) == 0)
--            packages_score_ignore(pkgs, src->ign_patterns, 1);
--    }
--
-     return pkgs;
- }
- 
-diff --git a/tests/Makefile.am b/tests/Makefile.am
-index 04654c67..ff08ccb9 100644
---- a/tests/Makefile.am
-+++ b/tests/Makefile.am
-@@ -2,7 +2,7 @@ CPPFLAGS = @CPPFLAGS@ @CHECK_CFLAGS@
- LDADD = $(top_builddir)/libpoldek.la @CHECK_LIBS@
- 
- check_PROGRAMS = test_match test_env test_pmdb test_op test_config \
--		 test_store test_cmp test_booldeps
-+		 test_store test_cmp test_booldeps test_ignore_merge
- 
- TESTS = $(check_PROGRAMS)
- 
-diff --git a/tests/test_ignore_merge.c b/tests/test_ignore_merge.c
-new file mode 100644
-index 00000000..97e061ed
---- /dev/null
-+++ b/tests/test_ignore_merge.c
-@@ -0,0 +1,63 @@
-+#include "test.h"
-+#include "poldek.h"
-+#include "pkgdir/source.h"
-+
-+static int list_contains(tn_array *list, const char *s)
-+{
-+    int i;
-+    for (i = 0; i < n_array_size(list); i++)
-+        if (n_str_eq((const char *)n_array_nth(list, i), s))
-+            return 1;
-+    return 0;
-+}
-+
-+START_TEST (test_ignore_merge) {
-+    const char *path = "poldek_test_ignore_merge.conf";
-+    FILE *f = fopen(path, "w");
-+    fail_if(f == NULL, "cannot open %s for write", path);
-+    fprintf(f,
-+            "[global]\n"
-+            "ignore = foo* baz*\n"
-+            "\n"
-+            "[source]\n"
-+            "name = testsrc\n"
-+            "type = pndir\n"
-+            "url  = file:///tmp/poldek-test-nonexistent\n"
-+            "ignore = bar*\n");
-+    fclose(f);
-+
-+    poldeklib_init();
-+    struct poldek_ctx *ctx = poldek_new(0);
-+    fail_if(ctx == NULL, "poldek_new failed");
-+    fail_if(poldek_load_config(ctx, path, NULL, 0) == 0,
-+            "load_config failed");
-+    fail_if(poldek_setup(ctx) == 0, "poldek_setup failed");
-+
-+    tn_array *sources = poldek_get_sources(ctx);
-+    fail_if(sources == NULL, "no sources?");
-+
-+    struct source *src = NULL;
-+    int i;
-+    for (i = 0; i < n_array_size(sources); i++) {
-+        struct source *s = n_array_nth(sources, i);
-+        if (s->name && n_str_eq(s->name, "testsrc")) {
-+            src = s;
-+            break;
-+        }
-+    }
-+    fail_if(src == NULL, "testsrc not found in sources");
-+
-+    fail_unless(list_contains(src->ign_patterns, "foo*"),
-+                "global 'foo*' missing from merged ign_patterns");
-+    fail_unless(list_contains(src->ign_patterns, "baz*"),
-+                "global 'baz*' missing from merged ign_patterns");
-+    fail_unless(list_contains(src->ign_patterns, "bar*"),
-+                "per-source 'bar*' missing from merged ign_patterns");
-+
-+    n_array_free(sources);
-+    poldek_free(ctx);
-+    unlink(path);
-+}
-+END_TEST
-+
-+NTEST_RUNNER("ignore_merge", test_ignore_merge);
diff --git a/poldek-multilib-bare-name-install.patch b/poldek-multilib-bare-name-install.patch
deleted file mode 100644
index 13d269e..0000000
--- a/poldek-multilib-bare-name-install.patch
+++ /dev/null
@@ -1,247 +0,0 @@
-From 3d5addaec1f408f389f66e3ded712315f734158b Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Arkadiusz=20Mi=C5=9Bkiewicz?= <arekm at maven.pl>
-Date: Tue, 14 Apr 2026 13:59:26 +0200
-Subject: [PATCH 1/2] fix: in multilib mode, bare-name install should only
- install native arch
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-Problem: on a multilib system with multiple architecture repos (e.g.
-th, th-i686, th-x86_64), running "install curl" resolves curl from ALL
-architectures and pulls in a massive foreign-arch dependency chain
-(40+ packages). It also emits a misleading "ambiguous name" warning.
-The user only wanted the native-arch package.
-
-Solution: add ARG_PACKAGES_RESOLV_PREFER_NATIVE flag and filter in
-resolve_masks() — the resolution layer where user masks are matched
-to packages. When the flag is set (install path only, not ls/search):
-
-- Bare-name matches (strcmp with pkg->name) skip non-native, non-noarch
-  packages via the multilib_skip_foreign() helper
-- Glob/fnmatch matches are unaffected — "install curl*" installs all
-- Explicit names like "install curl-8.19.0-1.i686" go through fnmatch
-  and are also unaffected
-
-This eliminates the false "ambiguous name" warning (matches_bycmp now
-only counts native-arch matches) and produces a helpful suggestion when
-no native arch exists at all. The no-native-arch case sets rc=0 (fails
-the transaction) like "no such package" does — the user must use an
-explicit name.
-
-The install validation function (poldek_ts_validate_args_with_stubs)
-has no multilib awareness — arch policy lives entirely in the
-resolution layer where it belongs.
-
-Examples:
-  install curl                -> installs only curl.x86_64 (native)
-  install curl-8.19.0-1.i686 -> installs curl.i686 (explicit)
-  install curl*               -> installs all matching arches (glob)
-  ls curl                     -> shows all arches (no PREFER_NATIVE)
----
- arg_packages.c | 68 +++++++++++++++++++++++++++++++++++++++++++++-----
- arg_packages.h |  4 +++
- 2 files changed, 66 insertions(+), 6 deletions(-)
-
-diff --git a/arg_packages.c b/arg_packages.c
-index 8b2d2cdb..d8def6b9 100644
---- a/arg_packages.c
-+++ b/arg_packages.c
-@@ -457,6 +457,31 @@ tn_array *resolve_bycap(struct arg_packages *aps, struct pkgset *ps,
-     return pkgs;
- }
- 
-+/*
-+  multilib: should this package be skipped for a bare-name mask match?
-+
-+  Problem: "install curl" on a multilib system resolves to curl.x86_64,
-+  curl.i686, curl.x32 — pulling in a massive foreign-arch dependency
-+  chain.  The user just wanted the native-arch package.
-+
-+  This only applies to bare-name matches (strcmp with pkg->name).
-+  Explicit names ("install curl-1.0-1.i686") and globs ("install curl*")
-+  go through the fnmatch path and are not affected.
-+
-+  Returns true if the package should be skipped:
-+  - caller asked for PREFER_NATIVE (install path, not ls/search)
-+  - multilib mode is on
-+  - package is not noarch (noarch is always installable)
-+  - package arch score != 1 (not the native/base architecture)
-+*/
-+static int multilib_skip_foreign(const struct pkg *pkg, unsigned flags)
-+{
-+    return (flags & ARG_PACKAGES_RESOLV_PREFER_NATIVE) &&
-+           poldek_conf_MULTILIB &&
-+           !pkg_is_noarch(pkg) &&
-+           pkg_arch_score(pkg) != 1;
-+}
-+
- static
- int resolve_masks(tn_array *re,
-                   struct arg_packages *aps, tn_array *avpkgs,
-@@ -465,6 +490,7 @@ int resolve_masks(tn_array *re,
- {
-     int i, j, nmasks, rc = 1;
-     int *matches, *matches_bycmp;
-+    const char **first_foreign_id;
- 
-     nmasks = n_array_size(aps->package_masks);
- 
-@@ -474,6 +500,12 @@ int resolve_masks(tn_array *re,
-     matches_bycmp = alloca(nmasks * sizeof(*matches_bycmp));
-     memset(matches_bycmp, 0, nmasks * sizeof(*matches_bycmp));
- 
-+    /* first_foreign_id[j]: when a bare-name mask skips a foreign-arch
-+       package, remember its pkg_id (e.g. "curl-8.19.0-1.i686") so we
-+       can suggest it in the warning if no native arch exists at all */
-+    first_foreign_id = alloca(nmasks * sizeof(*first_foreign_id));
-+    memset(first_foreign_id, 0, nmasks * sizeof(*first_foreign_id));
-+
-     for (i=0; i < n_array_size(avpkgs); i++) {
-         struct pkg *pkg = n_array_nth(avpkgs, i);
- 
-@@ -494,11 +526,16 @@ int resolve_masks(tn_array *re,
- 
-             DBGF("%s cmp %s or %s\n", mask, pkg->name, pkg_id(pkg));
-             if (strcmp(mask, pkg->name) == 0) {
--                if (re)
--                    n_array_push(re, pkg_link(pkg));
--
--                matches_bycmp[j]++;
--                matches[j]++;
-+                if (multilib_skip_foreign(pkg, flags)) {
-+                    if (!first_foreign_id[j])
-+                        first_foreign_id[j] = pkg_id(pkg);
-+                } else {
-+                    if (re)
-+                        n_array_push(re, pkg_link(pkg));
-+
-+                    matches_bycmp[j]++;
-+                    matches[j]++;
-+                }
- 
-             } else if (fnmatch(mask, pkg_id(pkg), 0) == 0) {
-                 if (re)
-@@ -521,6 +558,17 @@ int resolve_masks(tn_array *re,
-             }
-         }
- 
-+        /* multilib: bare name matched only foreign-arch packages;
-+           fail like "no such package" — the user must be explicit */
-+        if (unmatched && first_foreign_id[j]) {
-+            if (!quiet)
-+                logn(LOGWARN, _("%s: no native arch package available;"
-+                     " to install use explicit name, e.g.: %s"),
-+                     mask, first_foreign_id[j]);
-+            rc = 0;
-+            continue;
-+        }
-+
-         if (unmatched && (flags & ARG_PACKAGES_RESOLV_MISSINGOK) == 0) {
-             if (!quiet)
-                 logn(LOGERR, _("%s: no such package"), mask);
-@@ -698,7 +746,15 @@ int arg_packages__validate_with_stubs(struct arg_packages *aps, tn_array *stubpk
-         re = *resolved;
-     }
- 
--    if (!resolve_masks(re, aps, stubpkgs, NULL, 0, quiet))
-+    /* install path: in multilib mode, bare package names (e.g. "install curl")
-+       should resolve only to native-arch packages.  The ls/search path
-+       (arg_packages_resolve) does not set this flag, so "ls curl" still
-+       shows all architectures. */
-+    unsigned resolve_flags = 0;
-+    if (poldek_conf_MULTILIB)
-+        resolve_flags |= ARG_PACKAGES_RESOLV_PREFER_NATIVE;
-+
-+    if (!resolve_masks(re, aps, stubpkgs, NULL, resolve_flags, quiet))
-         return 0;
- 
-     if (n_array_size(aps->packages) > 0) {
-diff --git a/arg_packages.h b/arg_packages.h
-index cbeb163a..2de984c4 100644
---- a/arg_packages.h
-+++ b/arg_packages.h
-@@ -50,6 +50,10 @@ EXPORT int arg_packages_setup(struct arg_packages *aps, struct pm_ctx *ctx);
- #define ARG_PACKAGES_RESOLV_CAPSINLINE  (1 << 4)/* add packages found by caps
-                                                    to resolved packages */
- #define ARG_PACKAGES_RESOLV_WARN_ONLY   (1 << 5)/* warn only*/
-+#define ARG_PACKAGES_RESOLV_PREFER_NATIVE (1 << 6)/* multilib: "install curl"
-+                                                      resolves only to native arch;
-+                                                      "install curl-1.0-1.i686" and
-+                                                      "install curl*" still match all */
- 
- int arg_packages__validate_with_stubs(struct arg_packages *aps, tn_array *stubpkgs,
-                                       tn_array **resolved, int quiet);
--- 
-2.53.0
-
-From e924bff4180e6fcba25d8c8d6e12ef5fbe582f52 Mon Sep 17 00:00:00 2001
-From: =?UTF-8?q?Arkadiusz=20Mi=C5=9Bkiewicz?= <arekm at maven.pl>
-Date: Tue, 14 Apr 2026 13:03:34 +0200
-Subject: [PATCH 2/2] test: add multilib bare-name vs explicit arch install
- tests
-
-Verify that bare-name install picks only native arch, explicit
-name-version.arch installs foreign arch, and glob patterns install
-all matching architectures.
----
- tests/sh/07-depsolver | 43 +++++++++++++++++++++++++++++++++++++++++++
- 1 file changed, 43 insertions(+)
-
-diff --git a/tests/sh/07-depsolver b/tests/sh/07-depsolver
-index 5c95e6b0..a4ca2d85 100755
---- a/tests/sh/07-depsolver
-+++ b/tests/sh/07-depsolver
-@@ -965,6 +965,49 @@ xtestUninstallHold() {
-     try_uninstall a "a-1-1,a-devel-1-1"
- }
- 
-+# bare-name install in multilib should only install native arch
-+testInstallBareNameMultilibNativeOnly() {
-+    ORIG_POLDEK_INSTALL="$POLDEK_INSTALL"
-+    POLDEK_INSTALL="$POLDEK_INSTALL -Omultilib=1"
-+
-+    msgn "Preparing repositories..."
-+    build a 1-1 -a x86_64 -f "/hello.x86_64"
-+    build a 1-1 -a i686 -f "/hello.i686"
-+
-+    msgn "Bare-name install should pick only native arch"
-+    try_install a "a-1-1.x86_64"
-+
-+    POLDEK_INSTALL="$ORIG_POLDEK_INSTALL"
-+}
-+
-+# explicit name-version.arch should install that specific arch
-+testInstallExplicitArchMultilib() {
-+    ORIG_POLDEK_INSTALL="$POLDEK_INSTALL"
-+    POLDEK_INSTALL="$POLDEK_INSTALL -Omultilib=1"
-+
-+    msgn "Preparing repositories..."
-+    build a 1-1 -a x86_64 -f "/hello.x86_64"
-+    build a 1-1 -a i686 -f "/hello.i686"
- 
-+    msgn "Explicit arch should install that specific arch"
-+    try_install a-1-1.i686 "a-1-1.i686"
-+
-+    POLDEK_INSTALL="$ORIG_POLDEK_INSTALL"
-+}
-+
-+# glob pattern should install all matching arches
-+testInstallGlobMultilibAllArches() {
-+    ORIG_POLDEK_INSTALL="$POLDEK_INSTALL"
-+    POLDEK_INSTALL="$POLDEK_INSTALL -Omultilib=1"
-+
-+    msgn "Preparing repositories..."
-+    build a 1-1 -a x86_64 -f "/hello.x86_64"
-+    build a 1-1 -a i686 -f "/hello.i686"
-+
-+    msgn "Glob should install all matching arches"
-+    try_install_with_ask_allowed "a*" "a-1-1.x86_64,a-1-1.i686"
-+
-+    POLDEK_INSTALL="$ORIG_POLDEK_INSTALL"
-+}
- 
- . ./sh/lib/shunit2
--- 
-2.53.0
-
diff --git a/poldek-nocolor-cmp.patch b/poldek-nocolor-cmp.patch
deleted file mode 100644
index abfa538..0000000
--- a/poldek-nocolor-cmp.patch
+++ /dev/null
@@ -1,56 +0,0 @@
-    fix: do not treat uncolored packages from different architectures as interchangeable
-    
-    -devel packages typically have HEADERCOLOR=0 (no ELF binaries, only
-    headers, symlinks, pkg-config and libtool files). pkg_is_colored_like()
-    was returning 1 ("same kind") whenever at least one package had color 0,
-    causing poldek to incorrectly obsolete e.g. xz-devel.x32 when installing
-    xz-devel.x86_64, even though they install to different paths
-    (/usr/libx32 vs /usr/lib64) and are not interchangeable.
-    
-    The uncolored fallback was changed to a blanket "return 1" in commit
-    dad87fe8 (LP#299685, 2010). The earlier commit 1fd8aa21 (2008) disabled
-    arch-based fallback with the comment "rpm relies on colors only".
-    
-    Regardless of what older RPM versions did, RPM 6.0.1's checkAdded() and
-    rpmtsTeIterator() (lib/depends.cc) use arch string matching as a second
-    layer alongside colors in multilib transactions.
-    
-    Fix pkg_is_colored_like() to fall back to arch comparison when at least
-    one package is uncolored, and remove the redundant "any uncolored"
-    bypass in obs_filter() which was a second path for the same bug.
-
-diff --git a/install3/obsoletes.c b/install3/obsoletes.c
-index c70c04da..d137aeac 100644
---- a/install3/obsoletes.c
-+++ b/install3/obsoletes.c
-@@ -132,10 +132,6 @@ int obs_filter(struct pkgdb *db, const struct pm_dbrec *dbrec, void *apkg)
-     if (pkg_is_colored_like(&dbpkg, pkg))
-         return 1;
- 
--    /* any uncolored -> rpm allows upgrade */
--    if (dbpkg.color == 0 || pkg->color == 0)
--        return 1;
--
-     return 0;
- }
- 
-diff --git a/pkgcmp.c b/pkgcmp.c
-index aa8af162..e75ddf5b 100644
---- a/pkgcmp.c
-+++ b/pkgcmp.c
-@@ -80,8 +80,13 @@ int pkg_is_colored_like(const struct pkg *candidate, const struct pkg *pkg)
-     if (pkg->color && candidate->color)
-         return (pkg->color & candidate->color) > 0;
- 
--    /* either new or old package contains no binary files, let it happen */
--    return 1;
-+    /* When at least one package has no ELF binaries (color 0), fall back
-+       to arch comparison. This prevents treating uncolored packages from
-+       different architectures (e.g. x32 vs x86_64 -devel packages) as
-+       interchangeable. RPM 6.0.1 itself uses arch string matching as a
-+       second layer alongside colors (see checkAdded() and
-+       rpmtsTeIterator() in rpm's lib/depends.cc). */
-+    return pkg_cmp_same_arch(candidate, pkg);
- }
- 
- /* ret : 1 if pkg is cappable to upgrade arch<=>arch, arch<=>noarch */
diff --git a/poldek-scoring-evr.patch b/poldek-scoring-evr.patch
deleted file mode 100644
index f83d6ad..0000000
--- a/poldek-scoring-evr.patch
+++ /dev/null
@@ -1,101 +0,0 @@
-commit 2d65d2e1063e0f9c7d2196eb9170cd3426192462
-Author: Arkadiusz Miśkiewicz <arekm at maven.pl>
-Date:   Wed Apr 15 21:29:46 2026 +0200
-
-    Break ties between equivalent providers by preferring highest EVR
-    
-    When multiple packages provide the same capability and all score
-    equally on every signal (satisfiability, name-prefix match, arch,
-    conflicts, upgrade), the winner was determined by alphabetical sort
-    order of the package name. This systematically picked the oldest
-    available version — e.g. php4-program over php85-program when 23
-    packages provide /usr/bin/php.
-    
-    Add an EVR (epoch-version-release) comparison as a tiebreaker in
-    do_select_best_pkg(). When two candidates have identical scores,
-    the one with the higher EVR wins. This only fires when all other
-    scoring signals are equal — it never overrides a score-based
-    preference. Version constraints from the requirement are enforced
-    before candidates reach scoring, so the tiebreaker can only choose
-    among packages that already satisfy the requirement.
-    
-    Apply the same tiebreaker in prepare_icap() for the --caplookup
-    path: when no provider is installed, pick highest EVR instead of
-    alphabetically first.
-    
-    Tested on 133 PHP packages and 49 non-PHP packages against a
-    builder-like chroot (base system, no PHP installed):
-    
-    PHP: Before the fix, 44 out of 133 packages pulled in a mix of
-    PHP versions (e.g. php4 + php52 + php53 + php74 simultaneously).
-    After the fix, all 133 resolve to a single consistent version
-    (php85), except 2 packages that depend on extensions only available
-    for old PHP (php-mnogosearch, php-xcache).
-    
-    Java: openjdk8-jre (epoch=1) now selected over icedtea8-jre
-    (no epoch) for ant and maven.
-    
-    Init system: systemd-init (epoch=1) now selected over SysVinit
-    (no epoch) for packages requiring an init daemon.
-
-diff --git a/install3/misc.c b/install3/misc.c
-index b1940972..c6b7c0cb 100644
---- a/install3/misc.c
-+++ b/install3/misc.c
-@@ -370,6 +370,22 @@ static int do_select_best_pkg(int indent, struct i3ctx *ictx,
-         if (sc->score > best_score) {
-             best_score = sc->score;
-             i_best = i;
-+        } else if (sc->score == best_score) {
-+            /* EVR tiebreaker: when scores are tied, prefer the
-+               candidate with the highest epoch-version-release.
-+               Without this, the winner is the first candidate in
-+               alphabetical name order, which systematically picks
-+               the oldest version (e.g. php4-program over
-+               php85-program when 23 packages provide /usr/bin/php).
-+               This is safe because version constraints are enforced
-+               before candidates reach scoring — only packages that
-+               satisfy the requirement are considered here.
-+               See also prepare_icap() for the --caplookup path. */
-+            struct pkg *pbest = n_array_nth(candidates, i_best);
-+            struct pkg *pcand = n_array_nth(candidates, i);
-+            if (pkg_cmp_evr(pcand, pbest) > 0) {
-+                i_best = i;
-+            }
-         }
- 
-         if (sc->satscore > best_satscore) {
-diff --git a/install3/preinstall.c b/install3/preinstall.c
-index b237c1b4..1e7e5934 100644
---- a/install3/preinstall.c
-+++ b/install3/preinstall.c
-@@ -90,8 +90,16 @@ int prepare_icap(struct poldek_ts *ts, const char *capname, tn_array *pkgs)
-             }
-         }
- 
--        if (pkg == NULL)
-+        if (pkg == NULL) {
-+            /* EVR tiebreaker: pick highest EVR instead of
-+               alphabetically first; see also do_select_best_pkg() */
-             pkg = n_array_nth(pkgs, 0);
-+            for (i = 1; i < n_array_size(pkgs); i++) {
-+                struct pkg *p = n_array_nth(pkgs, i);
-+                if (pkg_cmp_evr(p, pkg) > 0)
-+                    pkg = p;
-+            }
-+        }
- 
-         pkg_hand_mark(ts->pms, pkg);
-         return 1;
-diff --git a/pkgcmp.c b/pkgcmp.c
-index e75ddf5b..05fce886 100644
---- a/pkgcmp.c
-+++ b/pkgcmp.c
-@@ -170,7 +170,6 @@ int pkg_cmp_evr(const struct pkg *p1, const struct pkg *p2)
-     return rc;
- }
- 
--
- int pkg_cmp_name_evr(const struct pkg *p1, const struct pkg *p2)
- {
-     register int rc;
diff --git a/poldek-search-i-opt.patch b/poldek-search-i-opt.patch
deleted file mode 100644
index c127e45..0000000
--- a/poldek-search-i-opt.patch
+++ /dev/null
@@ -1,138 +0,0 @@
-diff --git a/cli/search.c b/cli/search.c
-index 4d6f06a..6db2ebc 100644
---- a/cli/search.c
-+++ b/cli/search.c
-@@ -61,6 +61,7 @@ static int                    pcre_established = 0;
- struct pattern {
-     int              type;
-     char             *regexp;
-+    int              ignore_case;
-     int              fnmatch_flags;
-     unsigned         pcre_flags;
-     pcre             *pcre;
-@@ -71,6 +72,7 @@ static error_t parse_opt(int key, char *arg, struct argp_state *state);
- static int search(struct cmdctx *cmdctx);
- 
- #define OPT_PATTERN_PCRE     (1 << 10)
-+#define OPT_PATTERN_ICASE    (1 << 11)
- 
- #define OPT_SEARCH_CAP       (1 << 0)
- #define OPT_SEARCH_REQ       (1 << 1)
-@@ -95,7 +97,7 @@ static int search(struct cmdctx *cmdctx);
- 			    OPT_SEARCH_CHANGELOG)
- 
- 
--#define OPT_NO_SEARCHSW    OPT_PATTERN_PCRE
-+#define OPT_NO_SEARCHSW    (OPT_PATTERN_PCRE | OPT_PATTERN_ICASE)
- 
- static struct argp_option options[] = {
-     { "provides",    'p', 0, 0, N_("Search capabilities"), 1},
-@@ -106,6 +108,7 @@ static struct argp_option options[] = {
-     { "summary",     's', 0, 0, N_("Search summaries, urls and license"), 1},
-     { "description", 'd', 0, 0, N_("Search descriptions"), 1},
-     { "group",       'g', 0, 0, N_("Search groups"), 1 },
-+    { "ignore-case", 'i', 0, 0, N_("Ignore case when matching"), 1},
-     { "files",       'f', 0, 0, N_("Search file list"), 1},
-     { NULL,          'l', 0,  OPTION_ALIAS, 0, 1},
-     { "changelog",   'L', 0, 0, N_("Search changelogs"), 1},
-@@ -124,7 +127,10 @@ struct poclidek_cmd command_search = {
-     options, parse_opt,
-     NULL, search,
-     NULL, NULL,
--    N_("With --perlre pattern must be supplied as:\n"
-+    N_("Patterns are matched case-sensitively by default.\n"
-+       "Use -i, --ignore-case to ignore case.\n"
-+       "\n"
-+       "With --perlre pattern must be supplied as:\n"
-        "     <delimiter>perl-regexp<delimiter>[imsx]\n"
-        "  For example to find the packages containing foo.bar do:\n"
-        "     search --perlre /foo\\.bar/\n"
-@@ -204,10 +210,15 @@ static struct pattern *build_pattern(struct cmdctx *cmdctx, char *arg)
-         pt->type = PATTERN_FMASK;
- 
-     pt->regexp = n_strdup(regexp);
-+    pt->ignore_case = ((cmdctx->_flags & OPT_PATTERN_ICASE) != 0);
-     pt->fnmatch_flags = 0;
-     pt->pcre_flags = flags;
-     pt->pcre = NULL;
-     pt->pcre_extra = NULL;
-+
-+    if (pt->ignore_case && pt->type == PATTERN_PCRE)
-+        pt->pcre_flags |= PCRE_CASELESS;
-+
-     return pt;
- }
- 
-@@ -237,6 +248,10 @@ error_t parse_opt(int key, char *arg, struct argp_state *state)
-             cmdctx->_flags |= OPT_SEARCH_GROUP;
-             break;
- 
-+        case 'i':
-+            cmdctx->_flags |= OPT_PATTERN_ICASE;
-+            break;
-+
-         case 'o':
-             cmdctx->_flags |= OPT_SEARCH_OBSL;
-             break;
-@@ -322,7 +337,13 @@ int pattern_compile(struct pattern *pt, int ntimes)
-     n_assert(pt->pcre_extra == NULL);
- 
- #ifdef FNM_CASEFOLD
--    pt->fnmatch_flags |= FNM_CASEFOLD;
-+    if (pt->ignore_case)
-+        pt->fnmatch_flags |= FNM_CASEFOLD;
-+#else
-+    if (pt->ignore_case && pt->type == PATTERN_FMASK) {
-+        logn(LOGERR, _("search: case-insensitive glob matching is not supported on this platform"));
-+        return 0;
-+    }
- #endif
- 
-     if (pt->type != PATTERN_PCRE)
-diff --git a/doc/manual.xml b/doc/manual.xml
-index 9d8923c..6c4fcdc 100644
---- a/doc/manual.xml
-+++ b/doc/manual.xml
-@@ -846,7 +846,9 @@ Syntax of command is:
- search [OPTION...] PATTERN [PACKAGE...]
- </screen>
- Where <emphasis>PATTERN</emphasis> is a glob or, with <option>--perlre</option>, 
--Perl regular expression.  
-+Perl regular expression. Matching is case-sensitive by default; use
-+<option>-i</option>, <option>--ignore-case</option> for case-insensitive
-+matching.
- 
- For instance to find the packages that contains <filename>/usr/sbin/ab</filename> file:
- <screen>
-diff --git a/doc/poldek.1 b/doc/poldek.1
-index fd7982e..273e100 100644
---- a/doc/poldek.1
-+++ b/doc/poldek.1
-@@ -51,6 +51,12 @@ is a full\-featured packages management utility, basically designed to work with
- This manual page covers command line option reference, the full documentation for poldek is available as a Texinfo manual\&. Do
- \fBinfo poldek\fR
- to get it\&.
-+.PP
-+In shell mode, the
-+\fBsearch\fR
-+command matches case\-sensitively by default; use
-+\fBsearch \-i\fR
-+for case\-insensitive matching\&.
- .SH "OPTIONS REFERENCE"
- .SS "Repository index creation"
- .PP
-diff --git a/doc/poldek.1.xml b/doc/poldek.1.xml
-index e1a923a..a7eeff1 100644
---- a/doc/poldek.1.xml
-+++ b/doc/poldek.1.xml
-@@ -44,6 +44,10 @@ tasks like installation, upgrading or removal.
- <para>
- This manual page covers command line option reference, the full documentation
- for poldek is available as a Texinfo manual. Do <command>info poldek</command> to get it.
-+</para>
-+<para>
-+In shell mode, the <command>search</command> command matches case-sensitively
-+by default; use <command>search -i</command> for case-insensitive matching.
- </para>
-   </refsect1>
- 
diff --git a/poldek-url-keep-port.patch b/poldek-url-keep-port.patch
deleted file mode 100644
index 7cdf0ae..0000000
--- a/poldek-url-keep-port.patch
+++ /dev/null
@@ -1,50 +0,0 @@
-    fix: keep an explicit port in redirect targets
-
-    Two places dropped it:
-
-    - vf_request_new() reassembled req->url without the port, so everything
-      that re-parses that string - the redirect retries in vf_stat() and
-      vfile__vf_fetch(), and the absolute URI sent to a proxy - silently
-      went to the protocol default port.
-    - do_vfn() resolves a relative Location against the request itself and
-      built that URL from req->host alone, so a repository on a
-      non-default port reconnected to 80 or 443.
-
-diff --git a/vfile/vfreq.c b/vfile/vfreq.c
---- a/vfile/vfreq.c
-+++ b/vfile/vfreq.c
-@@ -278,8 +278,12 @@
-     else 
-         req->uri = n_strdupl(tmp, len);
- 
--    len = n_snprintf(tmp, sizeof(tmp), "%s://%s%s", rreq.proto, rreq.host,
--                     req->uri);
-+    if (rreq.port > 0)
-+        len = n_snprintf(tmp, sizeof(tmp), "%s://%s:%d%s", rreq.proto, rreq.host,
-+                         rreq.port, req->uri);
-+    else
-+        len = n_snprintf(tmp, sizeof(tmp), "%s://%s%s", rreq.proto, rreq.host,
-+                         req->uri);
-     req->url = n_strdupl(tmp, len);
-     req->port = rreq.port;
- 
-diff --git a/vfile/vfffmod.c b/vfile/vfffmod.c
---- a/vfile/vfffmod.c
-+++ b/vfile/vfffmod.c
-@@ -280,8 +280,14 @@
-         n_assert(cn->proto == VCN_PROTO_HTTP || cn->proto == VCN_PROTO_HTTPS);
- 
-         if (*vreq.redirected_to == '/') {
--            snprintf(topath, sizeof(topath), "http%s://%s%s", cn->proto == VCN_PROTO_HTTPS ? "s" : "" , req->host,
--                     vreq.redirected_to);
-+            char portstr[16] = "";
-+
-+            if (req->port > 0)
-+                snprintf(portstr, sizeof(portstr), ":%d", req->port);
-+
-+            snprintf(topath, sizeof(topath), "http%s://%s%s%s",
-+                     cn->proto == VCN_PROTO_HTTPS ? "s" : "", req->host,
-+                     portstr, vreq.redirected_to);
-             topathp = topath;
-         } else if (strncmp(vreq.redirected_to, "http://", 7) != 0)
-             foreign_proto = 1;
diff --git a/poldek-vcn-pool-reuse.patch b/poldek-vcn-pool-reuse.patch
deleted file mode 100644
index 4d67b0f..0000000
--- a/poldek-vcn-pool-reuse.patch
+++ /dev/null
@@ -1,53 +0,0 @@
-    fix: do not hand out unusable connections from the pool
-
-    Three ways a connection nobody can talk on gets used again:
-
-    - vcn_is_alive() short-circuits on a timestamp younger than
-      VCN_ALIVE_TTL without looking at cn->state, so a closed or dead
-      connection is reported alive.
-    - vcn_pool_vacuum() calls n_list_remove_ex() once, and on a
-      TN_LIST_UNIQ list that drops at most one match, so dead connections
-      accumulate in the pool.
-    - vftpcn_retr() leaves the control connection ALIVE after an aborted
-      data transfer even though the server's reply to the aborted RETR is
-      still unread; reusing it desynchronizes every following response, so
-      SIZE reads the reply to RETR, MDTM reads the reply to SIZE and so on.
-
-diff --git a/vfile/vfff/ftp.c b/vfile/vfff/ftp.c
---- a/vfile/vfff/ftp.c
-+++ b/vfile/vfff/ftp.c
-@@ -750,6 +750,7 @@
- 
-     if (!vfff_transfer_file(cn, req, total_size)) {
-         cn->sockfd = tmp_sockfd;
-+        cn->state = VCN_DEAD;   /* reply to the aborted RETR stays unread */
-         goto l_err;
-     }
- 
-diff --git a/vfile/vfff/vfff.c b/vfile/vfff/vfff.c
---- a/vfile/vfff/vfff.c
-+++ b/vfile/vfff/vfff.c
-@@ -342,6 +342,9 @@
- {
-     vfff_errno = 0;
- 
-+    if (cn->state != VCN_ALIVE)
-+        return 0;
-+
-     if (cn->ts_is_alive > 0) {
-         time_t ts = time(0);
- 
-diff --git a/vfile/vfffmod.c b/vfile/vfffmod.c
---- a/vfile/vfffmod.c
-+++ b/vfile/vfffmod.c
-@@ -88,7 +88,9 @@
- 
- void vcn_pool_vacuum(void)
- {
--    n_list_remove_ex(vcn_pool, NULL, toremove_cn_fakecmp);
-+    /* pool is TN_LIST_UNIQ, so one call drops at most one dead connection */
-+    while (n_list_remove_ex(vcn_pool, NULL, toremove_cn_fakecmp) > 0)
-+        ;
- }
- 
- static struct vcn *vcn_pool_do_connect(struct vf_request *req)
diff --git a/poldek-vfile-net-tests.patch b/poldek-vfile-net-tests.patch
deleted file mode 100644
index 9554823..0000000
--- a/poldek-vfile-net-tests.patch
+++ /dev/null
@@ -1,603 +0,0 @@
-    tests: network fault tests for the vfile fixes
-
-    Covers the crashes and the connection reuse bugs fixed by the patches
-    above: an unreadable PASV reply, a 227 with no address, an aborted data
-    transfer leaving the control connection desynchronized, a stat over a
-    failing protocol-changing redirect, and absolute and relative redirect
-    retries that have to keep the port.
-
-    net_servers.py is a minimal FTP server plus an HTTP redirector, both on
-    loopback, with the faults selected by the requested path.  test_vfnet is
-    built by "make check" but deliberately not run by it - it needs those
-    servers, so it is wired to its own target and started with "make check-net".
-
-diff --git a/vfile/tests/Makefile.am b/vfile/tests/Makefile.am
---- a/vfile/tests/Makefile.am
-+++ b/vfile/tests/Makefile.am
-@@ -1,10 +1,15 @@
- CPPFLAGS = @CPPFLAGS@ @CHECK_CFLAGS@
- LDADD = @CHECK_LIBS@ $(top_builddir)/vfile/libvfile.la
- 
--check_PROGRAMS = test_vfile test_vopen3
-+check_PROGRAMS = test_vfile test_vopen3 test_vfnet
- 
--TESTS = $(check_PROGRAMS)
--EXTRA_DIST = test.h
-+TESTS = test_vfile test_vopen3
-+EXTRA_DIST = test.h net_servers.py run_net_tests.sh
-+
-+# test_vfnet needs the helper servers, so it is not part of "make check";
-+# run it with: make check-net
-+check-net: test_vfnet$(EXEEXT)
-+	$(SHELL) $(srcdir)/run_net_tests.sh ./test_vfnet$(EXEEXT)
- 
- clean-local:
- 	-rm -f *.tmp core *.o *.bak *~ *% *\# TAGS gmon.out \#*\# dupa*
-diff --git a/vfile/tests/net_servers.py b/vfile/tests/net_servers.py
-new file mode 100644
---- /dev/null
-+++ b/vfile/tests/net_servers.py
-@@ -0,0 +1,250 @@
-+#!/usr/bin/env python3
-+"""
-+Network fault-injection servers for the vfile tests.
-+
-+Runs a minimal FTP server and a minimal HTTP redirector on loopback.  Both
-+are only good enough to drive vfile; the point is the faults they can inject.
-+
-+FTP faults are selected by the requested path (the server learns it from
-+SIZE/MDTM, which poldek sends before PASV):
-+  /pasv-drop/<name>     - abort the control connection when PASV arrives
-+  /pasv-nodigits/<name> - answer PASV with a 227 carrying no address at all
-+  /data-reset/<name>    - reset the data connection during RETR and leave
-+                          the reply to the aborted transfer unread
-+
-+HTTP serves nothing but redirects:
-+  /to-ftp/<name>        - 302 to ftp://127.0.0.1:<ftp port>/<name>
-+  /to-dead-https/<name> - 302 to https://127.0.0.1:1/<name>
-+  /rel-to-ftp/<name>    - 302 to the relative path /to-ftp/<name>
-+"""
-+
-+import argparse
-+import os
-+import socket
-+import struct
-+import sys
-+import threading
-+import time
-+
-+
-+def log(verbose, msg):
-+    if verbose:
-+        print(msg, file=sys.stderr, flush=True)
-+
-+
-+def listen(port):
-+    sock = socket.socket()
-+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-+    sock.bind(('127.0.0.1', port))
-+    sock.listen(16)
-+    return sock
-+
-+
-+def reset(sock):
-+    """Close so that the peer sees RST instead of an orderly EOF"""
-+    try:
-+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
-+                        struct.pack('ii', 1, 0))
-+        sock.close()
-+    except OSError:
-+        pass
-+
-+
-+class FTPSession:
-+    def __init__(self, sock, data_dir, verbose):
-+        self.sock = sock
-+        self.data_dir = data_dir
-+        self.verbose = verbose
-+        self.stream = sock.makefile('rwb', buffering=0)
-+        self.datasock = None
-+        self.path = ''
-+
-+    def log(self, msg):
-+        log(self.verbose, "ftpd: %s" % msg)
-+
-+    def send(self, line):
-+        self.log("> %s" % line.strip())
-+        self.stream.write(line.encode())
-+
-+    def abort(self):
-+        """makefile() holds a reference, so the socket only really closes
-+           once the stream is closed too"""
-+        self.stream.close()
-+        reset(self.sock)
-+
-+    def localpath(self):
-+        return os.path.join(self.data_dir, os.path.basename(self.path))
-+
-+    def open_datasock(self):
-+        self.datasock = listen(0)
-+        return self.datasock.getsockname()[1]
-+
-+    def serve(self):
-+        try:
-+            self.run()
-+        except OSError:         # client went away
-+            pass
-+
-+    def run(self):
-+        self.send("220 vfile test ftpd\r\n")
-+        while True:
-+            line = self.stream.readline()
-+            if not line:
-+                break
-+            line = line.decode(errors='replace').strip()
-+            self.log("< %s" % line)
-+            parts = line.split(None, 1)
-+            if not parts:
-+                continue
-+            cmd = parts[0].upper()
-+            arg = parts[1] if len(parts) > 1 else ''
-+
-+            if cmd in ('SIZE', 'MDTM', 'RETR'):
-+                self.path = arg
-+
-+            if cmd in ('PASV', 'EPSV') and 'pasv-drop' in self.path:
-+                self.log("!! dropping control connection at %s" % cmd)
-+                self.abort()
-+                return
-+
-+            if cmd in ('PASV', 'EPSV') and 'pasv-nodigits' in self.path:
-+                self.send("227 Entering Passive Mode\r\n")
-+                continue
-+
-+            if cmd == 'USER':
-+                self.send("331 password required\r\n")
-+            elif cmd == 'PASS':
-+                self.send("230 logged in\r\n")
-+            elif cmd in ('TYPE', 'NOOP'):
-+                self.send("200 ok\r\n")
-+            elif cmd == 'SIZE':
-+                if os.path.isfile(self.localpath()):
-+                    self.send("213 %d\r\n" % os.path.getsize(self.localpath()))
-+                else:
-+                    self.send("550 no such file\r\n")
-+            elif cmd == 'MDTM':
-+                if os.path.isfile(self.localpath()):
-+                    ts = time.gmtime(os.path.getmtime(self.localpath()))
-+                    self.send("213 %s\r\n" % time.strftime("%Y%m%d%H%M%S", ts))
-+                else:
-+                    self.send("550 no such file\r\n")
-+            elif cmd == 'PASV':
-+                port = self.open_datasock()
-+                self.send("227 Entering Passive Mode (127,0,0,1,%d,%d)\r\n"
-+                          % (port >> 8, port & 0xff))
-+            elif cmd == 'EPSV':
-+                self.send("229 Entering Extended Passive Mode (|||%d|)\r\n"
-+                          % self.open_datasock())
-+            elif cmd == 'REST':
-+                self.send("350 ok\r\n")
-+            elif cmd == 'RETR':
-+                self.retr()
-+            elif cmd == 'QUIT':
-+                self.send("221 bye\r\n")
-+                break
-+            else:
-+                self.send("500 unknown command\r\n")
-+
-+        self.sock.close()
-+
-+    def retr(self):
-+        if not os.path.isfile(self.localpath()) or self.datasock is None:
-+            self.send("550 no such file\r\n")
-+            return
-+
-+        size = os.path.getsize(self.localpath())
-+        self.send("150 Opening BINARY mode data connection (%d bytes)\r\n" % size)
-+        conn, _ = self.datasock.accept()
-+
-+        if 'data-reset' in self.path:
-+            self.log("!! resetting data connection")
-+            reset(conn)
-+            self.datasock.close()
-+            self.datasock = None
-+            self.send("426 Transfer aborted\r\n")
-+            return
-+
-+        with open(self.localpath(), 'rb') as f:
-+            conn.sendall(f.read())
-+        conn.close()
-+        self.datasock.close()
-+        self.datasock = None
-+        self.send("226 Transfer complete\r\n")
-+
-+
-+def http_session(sock, ftp_port, verbose):
-+    try:
-+        req = b""
-+        while b"\r\n\r\n" not in req:
-+            data = sock.recv(4096)
-+            if not data:
-+                return
-+            req += data
-+        line = req.split(b"\r\n")[0].decode(errors='replace')
-+        log(verbose, "httpd: < %s" % line)
-+        path = line.split()[1]
-+        name = os.path.basename(path)
-+
-+        if path.startswith("/to-ftp/"):
-+            to = "ftp://127.0.0.1:%d/%s" % (ftp_port, name)
-+        elif path.startswith("/rel-to-ftp/"):
-+            to = "/to-ftp/%s" % name
-+        elif path.startswith("/to-dead-https/"):
-+            to = "https://127.0.0.1:1/%s" % name
-+        else:
-+            sock.sendall(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n"
-+                         b"Connection: close\r\n\r\n")
-+            return
-+
-+        log(verbose, "httpd: > 302 %s" % to)
-+        sock.sendall(("HTTP/1.1 302 Found\r\nLocation: %s\r\nContent-Length: 0\r\n"
-+                      "Connection: close\r\n\r\n" % to).encode())
-+    except OSError:
-+        pass
-+    finally:
-+        try:
-+            sock.close()
-+        except OSError:
-+            pass
-+
-+
-+def main():
-+    parser = argparse.ArgumentParser(description='vfile network test servers')
-+    parser.add_argument('--ftp-port', type=int, default=0)
-+    parser.add_argument('--http-port', type=int, default=0)
-+    parser.add_argument('--data-dir', default='./data')
-+    parser.add_argument('--write-ports',
-+                        help='write "<ftp port> <http port>" to this file')
-+    parser.add_argument('--verbose', '-v', action='store_true')
-+    args = parser.parse_args()
-+
-+    verbose = args.verbose or bool(os.environ.get('VERBOSE'))
-+
-+    ftp_srv = listen(args.ftp_port)
-+    http_srv = listen(args.http_port)
-+    ftp_port = ftp_srv.getsockname()[1]
-+    http_port = http_srv.getsockname()[1]
-+
-+    if args.write_ports:
-+        with open(args.write_ports, 'w') as f:
-+            f.write("%d %d" % (ftp_port, http_port))
-+
-+    print("test servers on ftp://127.0.0.1:%d and http://127.0.0.1:%d"
-+          % (ftp_port, http_port), flush=True)
-+
-+    def accept_ftp():
-+        while True:
-+            conn, _ = ftp_srv.accept()
-+            session = FTPSession(conn, args.data_dir, verbose)
-+            threading.Thread(target=session.serve, daemon=True).start()
-+
-+    threading.Thread(target=accept_ftp, daemon=True).start()
-+
-+    while True:
-+        conn, _ = http_srv.accept()
-+        threading.Thread(target=http_session,
-+                         args=(conn, ftp_port, verbose), daemon=True).start()
-+
-+
-+if __name__ == '__main__':
-+    main()
-diff --git a/vfile/tests/test_vfnet.c b/vfile/tests/test_vfnet.c
-new file mode 100644
---- /dev/null
-+++ b/vfile/tests/test_vfnet.c
-@@ -0,0 +1,229 @@
-+/*
-+ * Network fault tests for vfile
-+ * Regression coverage for crashes and connection reuse on broken links
-+ */
-+
-+#include <stdio.h>
-+#include <stdlib.h>
-+#include <string.h>
-+#include <time.h>
-+#include <unistd.h>
-+#include <sys/types.h>
-+#include <sys/stat.h>
-+#include <fcntl.h>
-+#include <dirent.h>
-+
-+#include "../vfile.h"
-+#include "test.h"
-+#include <trurl/nstr.h>
-+
-+static char server_host[256] = "127.0.0.1";
-+static int ftp_port = 0;
-+static int http_port = 0;
-+static char tmpdir[PATH_MAX];
-+
-+#define TEST_FILE_SMALL "small.txt"
-+
-+static int vfile_initialized = 0;
-+
-+static void net_setup(void)
-+{
-+    static int verbose = 0;
-+
-+    if (getenv("VERBOSE"))
-+        verbose = 2;
-+
-+    if (!vfile_initialized) {
-+        vfile_configure(VFILE_CONF_VERBOSE, &verbose);
-+        vfile_configure(VFILE_CONF_CACHEDIR, "/tmp/vfile_test_cache");
-+        vfile_configure(VFILE_CONF_STUBBORN_RETR, 0); /* disable retrying */
-+        vfile_setup();
-+        vfile_initialized = 1;
-+    }
-+
-+    const char *host = getenv("TEST_SERVER_HOST");
-+    const char *fport = getenv("TEST_FTP_PORT");
-+    const char *hport = getenv("TEST_HTTP_PORT");
-+
-+    if (host) n_strncpy(server_host, host, sizeof(server_host));
-+    if (fport) ftp_port = atoi(fport);
-+    if (hport) http_port = atoi(hport);
-+
-+    snprintf(tmpdir, sizeof(tmpdir), "/tmp/vfile_nettest_%d_%ld", getpid(),
-+             (long)time(NULL));
-+    mkdir(tmpdir, 0755);
-+}
-+
-+static void net_teardown(void)
-+{
-+    DIR *dir;
-+    struct dirent *ent;
-+    char path[PATH_MAX];
-+
-+    /* successful fetches leave the file and its lock behind */
-+    if ((dir = opendir(tmpdir)) != NULL) {
-+        while ((ent = readdir(dir)) != NULL) {
-+            if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0)
-+                continue;
-+            if ((size_t)snprintf(path, sizeof(path), "%s/%s", tmpdir,
-+                                 ent->d_name) < sizeof(path))
-+                unlink(path);
-+        }
-+        closedir(dir);
-+    }
-+
-+    rmdir(tmpdir);
-+}
-+
-+static int fetch_ftp(const char *path)
-+{
-+    char url[512];
-+
-+    snprintf(url, sizeof(url), "ftp://%s:%d/%s", server_host, ftp_port, path);
-+    return vf_fetch(url, tmpdir, 0, NULL, NULL);
-+}
-+
-+static int have_file(const char *name)
-+{
-+    char dest[PATH_MAX];
-+
-+    if ((size_t)snprintf(dest, sizeof(dest), "%s/%s", tmpdir, name) >= sizeof(dest))
-+        ck_abort_msg("path truncated");
-+    return access(dest, F_OK) == 0;
-+}
-+
-+static void drop_file(const char *name)
-+{
-+    char dest[PATH_MAX];
-+
-+    if ((size_t)snprintf(dest, sizeof(dest), "%s/%s", tmpdir, name) >= sizeof(dest))
-+        ck_abort_msg("path truncated");
-+    unlink(dest);
-+}
-+
-+START_TEST(test_ftp_get_small)
-+{
-+    fail_if(ftp_port == 0);
-+
-+    fail_if(fetch_ftp(TEST_FILE_SMALL) == 0);
-+    fail_if(!have_file(TEST_FILE_SMALL));
-+}
-+END_TEST
-+
-+/* server drops the control connection while poldek waits for the PASV reply;
-+   used to segfault on vfff_set_err() with the unread response as format */
-+START_TEST(test_ftp_pasv_control_drop)
-+{
-+    fail_if(ftp_port == 0);
-+
-+    fail_if(fetch_ftp("pasv-drop/" TEST_FILE_SMALL) != 0);
-+}
-+END_TEST
-+
-+/* 227 reply with no address in it; the scan for the first digit used to run
-+   off the end of the response buffer */
-+START_TEST(test_ftp_pasv_no_digits)
-+{
-+    fail_if(ftp_port == 0);
-+
-+    fail_if(fetch_ftp("pasv-nodigits/" TEST_FILE_SMALL) != 0);
-+}
-+END_TEST
-+
-+/* an aborted data transfer leaves the reply to RETR unread, so the control
-+   connection must not be handed out again from the pool */
-+START_TEST(test_ftp_reuse_after_aborted_transfer)
-+{
-+    fail_if(ftp_port == 0);
-+
-+    /* first transfer succeeds and stamps the connection as alive */
-+    fail_if(fetch_ftp(TEST_FILE_SMALL) == 0);
-+    drop_file(TEST_FILE_SMALL);
-+
-+    fail_if(fetch_ftp("data-reset/" TEST_FILE_SMALL) != 0);
-+
-+    /* within VCN_ALIVE_TTL, so the pool would skip its NOOP probe */
-+    fail_if(fetch_ftp(TEST_FILE_SMALL) == 0);
-+    fail_if(!have_file(TEST_FILE_SMALL));
-+}
-+END_TEST
-+
-+/* stat over a redirect that switches protocol and then fails; the failed
-+   branch used to dereference the already freed request */
-+START_TEST(test_stat_redirect_failed)
-+{
-+    char url[512];
-+    struct vf_stat vfst;
-+
-+    fail_if(http_port == 0);
-+
-+    snprintf(url, sizeof(url), "http://%s:%d/to-dead-https/" TEST_FILE_SMALL,
-+             server_host, http_port);
-+
-+    fail_if(vf_stat(url, tmpdir, &vfst, NULL) != 0);
-+}
-+END_TEST
-+
-+/* the retried request is built from req->url, which used to lose the port */
-+START_TEST(test_redirect_keeps_port)
-+{
-+    char url[512];
-+
-+    fail_if(http_port == 0 || ftp_port == 0);
-+
-+    snprintf(url, sizeof(url), "http://%s:%d/to-ftp/" TEST_FILE_SMALL,
-+             server_host, http_port);
-+
-+    fail_if(vf_fetch(url, tmpdir, 0, NULL, NULL) == 0);
-+    fail_if(!have_file(TEST_FILE_SMALL));
-+}
-+END_TEST
-+
-+/* a relative Location is resolved against the request itself, which used to
-+   drop the port and send the retry to the protocol default */
-+START_TEST(test_relative_redirect_keeps_port)
-+{
-+    char url[512];
-+
-+    fail_if(http_port == 0 || ftp_port == 0);
-+
-+    snprintf(url, sizeof(url), "http://%s:%d/rel-to-ftp/" TEST_FILE_SMALL,
-+             server_host, http_port);
-+
-+    fail_if(vf_fetch(url, tmpdir, 0, NULL, NULL) == 0);
-+    fail_if(!have_file(TEST_FILE_SMALL));
-+}
-+END_TEST
-+
-+static Suite *vfnet_suite(void)
-+{
-+    Suite *s = suite_create("vfnet");
-+
-+    TCase *tc = tcase_create("net");
-+    tcase_add_checked_fixture(tc, net_setup, net_teardown);
-+    tcase_add_test(tc, test_ftp_get_small);
-+    tcase_add_test(tc, test_ftp_pasv_control_drop);
-+    tcase_add_test(tc, test_ftp_pasv_no_digits);
-+    tcase_add_test(tc, test_ftp_reuse_after_aborted_transfer);
-+    tcase_add_test(tc, test_stat_redirect_failed);
-+    tcase_add_test(tc, test_redirect_keeps_port);
-+    tcase_add_test(tc, test_relative_redirect_keeps_port);
-+    suite_add_tcase(s, tc);
-+
-+    return s;
-+}
-+
-+int main(int argc, char **argv)
-+{
-+    (void)argc;
-+    (void)argv;
-+    int nerr;
-+    Suite *s = vfnet_suite();
-+    SRunner *sr = srunner_create(s);
-+
-+    srunner_run_all(sr, CK_ENV);
-+    nerr = srunner_ntests_failed(sr);
-+    srunner_free(sr);
-+
-+    return (nerr == 0) ? EXIT_SUCCESS : EXIT_FAILURE;
-+}
-diff --git a/vfile/tests/run_net_tests.sh b/vfile/tests/run_net_tests.sh
-new file mode 100644
---- /dev/null
-+++ b/vfile/tests/run_net_tests.sh
-@@ -0,0 +1,74 @@
-+#!/bin/sh
-+# Network fault test runner
-+# Starts the test servers, creates test data dynamically, runs tests, cleans up
-+
-+set -e
-+
-+VERBOSE="${VERBOSE:-}"
-+
-+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-+PYTHON="${PYTHON:-python3}"
-+
-+vprint() {
-+    if [ -n "$VERBOSE" ]; then
-+        echo "$@"
-+    fi
-+}
-+
-+TEST_DATA_DIR="$(mktemp -d)"
-+PORT_FILE="$(mktemp)"
-+
-+cleanup() {
-+    if [ -n "$SRV_PID" ]; then
-+        kill "$SRV_PID" 2>/dev/null || true
-+        wait "$SRV_PID" 2>/dev/null || true
-+    fi
-+    rm -rf "$TEST_DATA_DIR" "$PORT_FILE"
-+}
-+trap cleanup EXIT
-+
-+printf 'Hello, World!\n' > "$TEST_DATA_DIR/small.txt"
-+
-+if ! "$PYTHON" --version >/dev/null 2>&1; then
-+    echo "ERROR: Python 3 not found. Set PYTHON environment variable."
-+    exit 1
-+fi
-+
-+vprint "Starting test servers..."
-+"$PYTHON" "$SCRIPT_DIR/net_servers.py" \
-+    --data-dir "$TEST_DATA_DIR" \
-+    --write-ports "$PORT_FILE" &
-+SRV_PID="$!"
-+
-+sleep 1
-+
-+if [ ! -s "$PORT_FILE" ]; then
-+    echo "ERROR: test servers did not start" >&2
-+    exit 1
-+fi
-+FTP_PORT=$(cut -d' ' -f1 "$PORT_FILE")
-+HTTP_PORT=$(cut -d' ' -f2 "$PORT_FILE")
-+vprint "FTP: ftp://127.0.0.1:$FTP_PORT  HTTP: http://127.0.0.1:$HTTP_PORT"
-+
-+# vfile honours the proxy environment, which would bypass the test servers
-+unset http_proxy https_proxy ftp_proxy
-+unset HTTP_PROXY HTTPS_PROXY FTP_PROXY
-+
-+export TEST_SERVER_HOST="127.0.0.1"
-+export TEST_FTP_PORT="$FTP_PORT"
-+export TEST_HTTP_PORT="$HTTP_PORT"
-+export TEST_DATA_DIR="$TEST_DATA_DIR"
-+
-+if [ $# -eq 0 ]; then
-+    cd "$SCRIPT_DIR"
-+    if [ -x ./test_vfnet ]; then
-+        ./test_vfnet
-+    elif [ -x ./.libs/test_vfnet ]; then
-+        ./.libs/test_vfnet
-+    else
-+        echo "ERROR: test_vfnet not found. Run 'make test_vfnet' first."
-+        exit 1
-+    fi
-+else
-+    "$@"
-+fi
diff --git a/poldek-vfstat-redirect-crash.patch b/poldek-vfstat-redirect-crash.patch
deleted file mode 100644
index 8fb11ca..0000000
--- a/poldek-vfstat-redirect-crash.patch
+++ /dev/null
@@ -1,27 +0,0 @@
-    fix: do not use the request after freeing it in vf_stat()
-
-    The redirect branch freed the request, set the pointer to NULL and then
-    read req->url from it - a guaranteed SIGSEGV whenever a HEAD gets a
-    protocol-changing redirect that afterwards fails.  The arguments were
-    swapped too; vf_stat() takes (url, destdir).
-
-    Copy the URL out before the free, the way vfile__vf_fetch() already
-    handles its own redirect retry.
-
-diff --git a/vfile/vfetch.c b/vfile/vfetch.c
---- a/vfile/vfetch.c
-+++ b/vfile/vfetch.c
-@@ -407,9 +407,12 @@
-             vfstat->vf_mtime = req->st_remote_mtime > 0 ? req->st_remote_mtime : 0;
- 
-         } else if (req->flags & VF_REQ_INT_REDIRECTED) {
-+            char redir_url[PATH_MAX];
-+
-+            snprintf(redir_url, sizeof(redir_url), "%s", req->url);
-             vf_request_free(req);
-             req = NULL;
--            rc = vf_stat(destdir, req->url, vfstat, NULL);
-+            rc = vf_stat(redir_url, destdir, vfstat, NULL);
- 
-         } else {
-             vfile_set_errno(mod->vfmod_name, req->req_errno);
================================================================

---- gitweb:

http://git.pld-linux.org/gitweb.cgi/packages/poldek.git/commitdiff/4aeb92834357540e7a0f8b9e4a72effdb289c3a0



More information about the pld-cvs-commit mailing list