From 752ee96a25b12d8cc9dd4b445989c4f056299c49 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Timo=20Ter=C3=A4s?= <timo.teras@iki.fi>
Date: Tue, 31 Dec 2024 17:28:58 +0200
Subject: [PATCH] db: refactor repository parsing and url printing

- pregenerate the needed repository urls
- get rid of apk_url_print and simplify url printing
---
 src/apk_database.h    |  20 ++++-
 src/apk_print.h       |  13 +--
 src/app_fetch.c       |   2 +-
 src/app_policy.c      |   4 +-
 src/app_update.c      |   6 +-
 src/app_version.c     |   4 +-
 src/ctype.c           |   2 +-
 src/database.c        | 192 +++++++++++++++++++++---------------------
 src/print.c           |  44 +++++-----
 test/unit/apk_test.h  |  14 +++
 test/unit/blob_test.c |  22 +++++
 test/unit/db_test.c   |  27 ++++++
 test/unit/meson.build |   1 +
 13 files changed, 208 insertions(+), 143 deletions(-)
 create mode 100644 test/unit/db_test.c

diff --git a/src/apk_database.h b/src/apk_database.h
index 5842eed7..43f2f5ae 100644
--- a/src/apk_database.h
+++ b/src/apk_database.h
@@ -136,18 +136,30 @@ struct apk_name {
 	char name[];
 };
 
+enum {
+	APK_REPOTYPE_INVALID = 0,
+	APK_REPOTYPE_NDX,
+	APK_REPOTYPE_V2,
+};
+
+struct apk_repoline {
+	apk_blob_t tag, url;
+	unsigned int type;
+};
+
 struct apk_repository {
-	const char *url;
 	struct apk_digest hash;
 	time_t mtime;
 	unsigned short tag_mask;
-	unsigned short url_is_file : 1;
 	unsigned short absolute_pkgname : 1;
 	unsigned short is_remote : 1;
 	unsigned short stale : 1;
 
 	apk_blob_t description;
 	apk_blob_t url_base;
+	apk_blob_t url_base_printable;
+	apk_blob_t url_index;
+	apk_blob_t url_index_printable;
 	apk_blob_t pkgname_spec;
 };
 
@@ -279,9 +291,9 @@ int apk_db_repository_check(struct apk_database *db);
 unsigned int apk_db_get_pinning_mask_repos(struct apk_database *db, unsigned short pinning_mask);
 struct apk_repository *apk_db_select_repo(struct apk_database *db, struct apk_package *pkg);
 
-int apk_repo_index_url(struct apk_database *db, struct apk_repository *repo, int *fd, char *buf, size_t len, struct apk_url_print *urlp);
+bool apk_repo_parse_line(apk_blob_t line, struct apk_repoline *rl);
 int apk_repo_index_cache_url(struct apk_database *db, struct apk_repository *repo, int *fd, char *buf, size_t len);
-int apk_repo_package_url(struct apk_database *db, struct apk_repository *repo, struct apk_package *pkg, int *fd, char *buf, size_t len, struct apk_url_print *urlp);
+int apk_repo_package_url(struct apk_database *db, struct apk_repository *repo, struct apk_package *pkg, int *fd, char *buf, size_t len);
 
 int apk_cache_download(struct apk_database *db, struct apk_repository *repo, struct apk_package *pkg, struct apk_progress *prog);
 
diff --git a/src/apk_print.h b/src/apk_print.h
index 6221517c..4c157694 100644
--- a/src/apk_print.h
+++ b/src/apk_print.h
@@ -21,18 +21,7 @@ const char *apk_error_str(int error);
 int apk_get_human_size_unit(apk_blob_t b);
 const char *apk_get_human_size(off_t size, off_t *dest);
 const char *apk_last_path_segment(const char *);
-
-struct apk_url_print {
-	const char *url;
-	const char *pwmask;
-	const char *url_or_host;
-	size_t len_before_pw;
-};
-
-void apk_url_parse(struct apk_url_print *, const char *);
-
-#define URL_FMT			"%.*s%s%s"
-#define URL_PRINTF(u)		(int)u.len_before_pw, u.url, u.pwmask, u.url_or_host
+apk_blob_t apk_url_sanitize(apk_blob_t url, struct apk_atom_pool *atoms);
 
 struct apk_out {
 	int verbosity, progress_fd;
diff --git a/src/app_fetch.c b/src/app_fetch.c
index 60106435..bd7461a5 100644
--- a/src/app_fetch.c
+++ b/src/app_fetch.c
@@ -173,7 +173,7 @@ static int fetch_package(struct apk_database *db, const char *match, struct apk_
 			return 0;
 	}
 
-	r = apk_repo_package_url(db, repo, pkg, &pkg_fd, pkg_url, sizeof pkg_url, NULL);
+	r = apk_repo_package_url(db, repo, pkg, &pkg_fd, pkg_url, sizeof pkg_url);
 	if (r < 0) goto err;
 
 	if (ctx->flags & FETCH_URL)
diff --git a/src/app_policy.c b/src/app_policy.c
index e679d082..3a6aa27d 100644
--- a/src/app_policy.c
+++ b/src/app_policy.c
@@ -49,10 +49,10 @@ zlib1g policy:
 				continue;
 			for (j = 0; j < db->num_repo_tags; j++) {
 				if (db->repo_tags[j].allowed_repos & p->pkg->repos)
-					apk_out(out, "    "BLOB_FMT"%s%s",
+					apk_out(out, "    " BLOB_FMT "%s" BLOB_FMT,
 						BLOB_PRINTF(db->repo_tags[j].tag),
 						j == 0 ? "" : " ",
-						repo->url);
+						BLOB_PRINTF(repo->url_base_printable));
 			}
 		}
 	}
diff --git a/src/app_update.c b/src/app_update.c
index 9e474809..0d01fc25 100644
--- a/src/app_update.c
+++ b/src/app_update.c
@@ -19,7 +19,6 @@ static int update_main(void *ctx, struct apk_ctx *ac, struct apk_string_array *a
 	struct apk_out *out = &ac->out;
 	struct apk_database *db = ac->db;
 	struct apk_repository *repo;
-	struct apk_url_print urlp;
 	int i;
 	const char *msg = "OK:";
 	char buf[64];
@@ -29,10 +28,9 @@ static int update_main(void *ctx, struct apk_ctx *ac, struct apk_string_array *a
 
 	for (i = APK_REPOSITORY_FIRST_CONFIGURED; i < db->num_repos; i++) {
 		repo = &db->repos[i];
-		apk_url_parse(&urlp, db->repos[i].url);
-		apk_msg(out, BLOB_FMT " [" URL_FMT "]",
+		apk_msg(out, BLOB_FMT " [" BLOB_FMT "]",
 			BLOB_PRINTF(repo->description),
-			URL_PRINTF(urlp));
+			BLOB_PRINTF(repo->url_base_printable));
 	}
 
 	if (db->repositories.unavailable || db->repositories.stale)
diff --git a/src/app_version.c b/src/app_version.c
index 2c2dc0fd..da0109ac 100644
--- a/src/app_version.c
+++ b/src/app_version.c
@@ -30,9 +30,9 @@ static int ver_indexes(struct apk_ctx *ac, struct apk_string_array *args)
 
 	for (i = APK_REPOSITORY_FIRST_CONFIGURED; i < db->num_repos; i++) {
 		repo = &db->repos[i];
-		apk_out(out, BLOB_FMT " [%s]",
+		apk_out(out, BLOB_FMT " [" BLOB_FMT "]",
 			BLOB_PRINTF(repo->description),
-			db->repos[i].url);
+			BLOB_PRINTF(repo->url_base_printable));
 	}
 
 	return 0;
diff --git a/src/ctype.c b/src/ctype.c
index 0fdcf5d6..a8e53359 100644
--- a/src/ctype.c
+++ b/src/ctype.c
@@ -26,7 +26,7 @@ static uint8_t apk_ctype[128] = {
 	[','] = DEPNAME,
 	['-'] = PKGNAME,
 	['.'] = PKGNAME,
-	[':'] = REPOSEP|DEPNAME,
+	[':'] = DEPNAME,
 	['<'] = DEPCOMP,
 	['='] = DEPCOMP,
 	['>'] = DEPCOMP,
diff --git a/src/database.c b/src/database.c
index 53a6cfc0..d4d5ae6b 100644
--- a/src/database.c
+++ b/src/database.c
@@ -674,22 +674,6 @@ static int apk_repo_subst(void *ctx, apk_blob_t key, apk_blob_t *to)
 	return 0;
 }
 
-int apk_repo_index_url(struct apk_database *db, struct apk_repository *repo,
-		       int *fd, char *buf, size_t len, struct apk_url_print *urlp)
-{
-	apk_blob_t uri = APK_BLOB_STR(repo->url);
-	int r;
-
-	r = apk_repo_fd(db, repo, fd);
-	if (r < 0) return r;
-
-	if (repo->url_is_file) r = apk_fmt(buf, len, BLOB_FMT, BLOB_PRINTF(uri));
-	else r = apk_fmt(buf, len, BLOB_FMT "/" BLOB_FMT "/APKINDEX.tar.gz", BLOB_PRINTF(uri), BLOB_PRINTF(*db->arches->item[0]));
-	if (r < 0) return r;
-	if (urlp) apk_url_parse(urlp, buf);
-	return 0;
-}
-
 int apk_repo_index_cache_url(struct apk_database *db, struct apk_repository *repo, int *fd, char *buf, size_t len)
 {
 	int r = apk_repo_fd(db, &db->repos[APK_REPOSITORY_CACHED], fd);
@@ -698,7 +682,7 @@ int apk_repo_index_cache_url(struct apk_database *db, struct apk_repository *rep
 }
 
 int apk_repo_package_url(struct apk_database *db, struct apk_repository *repo, struct apk_package *pkg,
-			 int *fd, char *buf, size_t len, struct apk_url_print *urlp)
+			 int *fd, char *buf, size_t len)
 {
 	struct apk_ctx *ac = db->ctx;
 	int r;
@@ -717,43 +701,43 @@ int apk_repo_package_url(struct apk_database *db, struct apk_repository *repo, s
 		r = apk_blob_subst(&buf[r], len - r, repo->pkgname_spec, apk_pkg_subst, pkg);
 	}
 	if (r < 0) return r;
-	if (urlp) apk_url_parse(urlp, buf);
 	return 0;
 }
 
 int apk_cache_download(struct apk_database *db, struct apk_repository *repo, struct apk_package *pkg, struct apk_progress *prog)
 {
 	struct apk_out *out = &db->ctx->out;
-	struct apk_url_print urlp;
 	struct apk_progress_istream pis;
 	struct apk_istream *is;
 	struct apk_ostream *os;
 	struct apk_extract_ctx ectx;
-	char download_url[PATH_MAX], cache_url[NAME_MAX];
+	const char *download_url;
+	char cache_url[NAME_MAX], package_url[PATH_MAX];
 	int r, download_fd, cache_fd, tee_flags = 0;
-	time_t mtime = 0;
+	time_t download_mtime = 0;
 
 	if (pkg != NULL) {
-		r = apk_repo_package_url(db, &db->repos[APK_REPOSITORY_CACHED], pkg, &cache_fd, cache_url, sizeof cache_url, NULL);
+		r = apk_repo_package_url(db, &db->repos[APK_REPOSITORY_CACHED], pkg, &cache_fd, cache_url, sizeof cache_url);
 		if (r < 0) return r;
-		r = apk_repo_package_url(db, repo, pkg, &download_fd, download_url, sizeof download_url, &urlp);
+		r = apk_repo_package_url(db, repo, pkg, &download_fd, package_url, sizeof package_url);
 		if (r < 0) return r;
 		tee_flags = APK_ISTREAM_TEE_COPY_META;
+		download_url = package_url;
 	} else {
 		r = apk_repo_index_cache_url(db, repo, &cache_fd, cache_url, sizeof cache_url);
 		if (r < 0) return r;
-		r = apk_repo_index_url(db, repo, &download_fd, download_url, sizeof download_url, &urlp);
-		if (r < 0) return r;
-		mtime = repo->mtime;
+		download_mtime = repo->mtime;
+		download_fd = AT_FDCWD;
+		download_url = repo->url_index.ptr;
 	}
 
-	if (!prog) apk_out_progress_note(out, "fetch " URL_FMT, URL_PRINTF(urlp));
+	if (!pkg && !prog) apk_out_progress_note(out, "fetch " BLOB_FMT, BLOB_PRINTF(repo->url_index_printable));
 	if (db->ctx->flags & APK_SIMULATE) return 0;
 
 	os = apk_ostream_to_file(cache_fd, cache_url, 0644);
 	if (IS_ERR(os)) return PTR_ERR(os);
 
-	is = apk_istream_from_fd_url_if_modified(download_fd, download_url, apk_db_url_since(db, mtime));
+	is = apk_istream_from_fd_url_if_modified(download_fd, download_url, apk_db_url_since(db, download_mtime));
 	is = apk_progress_istream(&pis, is, prog);
 	is = apk_istream_tee(is, os, tee_flags);
 	apk_extract_init(&ectx, db->ctx, NULL);
@@ -1416,7 +1400,8 @@ static int load_v3index(struct apk_extract_ctx *ectx, struct adb_obj *ndx)
 	}
 
 	apk_pkgtmpl_free(&tmpl);
-	if (num_broken) apk_warn(out, "Repository %s has %d packages without hash", repo->url, num_broken);
+	if (num_broken) apk_warn(out, "Repository " BLOB_FMT " has %d packages without hash",
+		BLOB_PRINTF(repo->url_index_printable), num_broken);
 	return r;
 }
 
@@ -1451,60 +1436,88 @@ static bool is_index_stale(struct apk_database *db, struct apk_repository *repo)
 	return (time(NULL) - st.st_mtime) > db->ctx->cache_max_age;
 }
 
-static int add_repository(struct apk_database *db, apk_blob_t _repository)
+static bool get_word(apk_blob_t *line, apk_blob_t *word)
 {
-	struct apk_repository *repo;
-	apk_blob_t brepo, btag, url_base, pkgname_spec;
-	int repo_num, r, tag_id = 0, url_is_file = 0, index_fd;
-	char index_url[PATH_MAX], *url;
+	apk_blob_cspn(*line, APK_CTYPE_REPOSITORY_SEPARATOR, word, line);
+	apk_blob_spn(*line, APK_CTYPE_REPOSITORY_SEPARATOR, NULL, line);
+	return word->len > 0;
+}
 
-	brepo = _repository;
-	btag = APK_BLOB_NULL;
-	if (brepo.ptr == NULL || brepo.len == 0 || *brepo.ptr == '#')
-		return 0;
+bool apk_repo_parse_line(apk_blob_t line, struct apk_repoline *rl)
+{
+	apk_blob_t word;
 
-	if (brepo.ptr[0] == '@') {
-		apk_blob_cspn(brepo, APK_CTYPE_REPOSITORY_SEPARATOR, &btag, &brepo);
-		apk_blob_spn(brepo, APK_CTYPE_REPOSITORY_SEPARATOR, NULL, &brepo);
-		tag_id = apk_db_get_tag_id(db, btag);
+	memset(rl, 0, sizeof *rl);
+	rl->type = APK_REPOTYPE_V2;
+
+	if (!get_word(&line, &word)) return false;
+	if (word.ptr[0] == '@') {
+		rl->tag = word;
+		if (!get_word(&line, &word)) return false;
 	}
+	if (apk_blob_ends_with(word, APK_BLOB_STRLIT(".adb"))) rl->type = APK_REPOTYPE_NDX;
+	rl->url = word;
+	return line.len == 0;
+}
 
-	url = apk_blob_cstr(brepo);
-	for (repo_num = 0; repo_num < db->num_repos; repo_num++) {
-		repo = &db->repos[repo_num];
-		if (strcmp(url, repo->url) == 0) {
-			repo->tag_mask |= BIT(tag_id);
-			free(url);
-			return 0;
-		}
+static int add_repository(struct apk_database *db, apk_blob_t line)
+{
+	struct apk_out *out = &db->ctx->out;
+	struct apk_repository *repo;
+	struct apk_repoline rl;
+	apk_blob_t url_base, url_index, pkgname_spec, dot = APK_BLOB_STRLIT(".");
+	char buf[PATH_MAX];
+	int tag_id = 0;
+
+	if (!line.ptr || line.len == 0 || line.ptr[0] == '#') return 0;
+	if (!apk_repo_parse_line(line, &rl)) {
+		apk_warn(out, "Unable to parse repository: " BLOB_FMT, BLOB_PRINTF(line));
+		return 0;
 	}
-	if (db->num_repos >= APK_MAX_REPOS) {
-		free(url);
-		return -1;
+	if (rl.type == APK_REPOTYPE_INVALID) {
+		apk_warn(out, "Unsupported repository: " BLOB_FMT, BLOB_PRINTF(line));
+		return 0;
 	}
+	if (rl.tag.ptr) tag_id = apk_db_get_tag_id(db, rl.tag);
 
-	if (apk_blob_ends_with(brepo, APK_BLOB_STRLIT(".adb"))) {
-		if (!apk_blob_rsplit(brepo, '/', &url_base, NULL)) url_base = APK_BLOB_STRLIT(".");
-		pkgname_spec = db->ctx->default_pkgname_spec;
-		url_is_file = 1;
-	} else {
-		url_base = apk_blob_trim_end(brepo, '/');
+	const char *index_file = NULL;
+	switch (rl.type) {
+	case APK_REPOTYPE_V2:
+		index_file = "APKINDEX.tar.gz";
+		break;
+	}
+	if (index_file) {
+		url_base = apk_blob_trim_end(rl.url, '/');
+		url_index = apk_blob_fmt(buf, sizeof buf, BLOB_FMT "/" BLOB_FMT "/%s", BLOB_PRINTF(url_base), BLOB_PRINTF(*db->arches->item[0]), index_file);
 		pkgname_spec = db->ctx->default_reponame_spec;
+	} else {
+		if (!apk_blob_rsplit(rl.url, '/', &url_base, NULL)) url_base = dot;
+		url_index = rl.url;
+		pkgname_spec = db->ctx->default_pkgname_spec;
 	}
 
-	repo_num = db->num_repos++;
-	repo = &db->repos[repo_num];
+	for (repo = &db->repos[APK_REPOSITORY_FIRST_CONFIGURED]; repo < &db->repos[db->num_repos]; repo++) {
+		if (apk_blob_compare(url_base, repo->url_base) != 0) continue;
+		if (apk_blob_compare(url_index, repo->url_index) != 0) continue;
+		repo->tag_mask |= BIT(tag_id);
+		return 0;
+	}
+	url_index = *apk_atomize_dup0(&db->atoms, url_index);
+	// url base is a prefix of url_index or '.'
+	if (url_base.ptr != dot.ptr) url_base = APK_BLOB_PTR_LEN(url_index.ptr, url_base.len);
+
+	if (db->num_repos >= APK_MAX_REPOS) return -1;
+	repo = &db->repos[db->num_repos++];
 	*repo = (struct apk_repository) {
-		.url = url,
-		.url_is_file = url_is_file,
-		.url_base = *apk_atomize_dup(&db->atoms, url_base),
+		.url_base = url_base,
+		.url_base_printable = apk_url_sanitize(url_base, &db->atoms),
+		.url_index = url_index,
+		.url_index_printable = apk_url_sanitize(url_index, &db->atoms),
 		.pkgname_spec = pkgname_spec,
-		.is_remote = apk_url_local_file(url) == NULL,
+		.is_remote = apk_url_local_file(url_index.ptr) == NULL,
 		.tag_mask = BIT(tag_id),
 	};
-	r = apk_repo_index_url(db, repo, &index_fd, index_url, sizeof index_url, NULL);
-	if (r < 0) return r;
-	apk_digest_calc(&repo->hash, APK_DIGEST_SHA256, index_url, strlen(index_url));
+	apk_digest_calc(&repo->hash, APK_DIGEST_SHA256, url_index.ptr, url_index.len);
 	if (is_index_stale(db, repo)) repo->stale = 1;
 	return 0;
 }
@@ -1513,22 +1526,18 @@ static void open_repository(struct apk_database *db, int repo_num)
 {
 	struct apk_out *out = &db->ctx->out;
 	struct apk_repository *repo = &db->repos[repo_num];
-	struct apk_url_print urlp;
 	const char *error_action = "constructing url";
 	unsigned int repo_mask = BIT(repo_num);
 	unsigned int available_repos = 0;
-	char index_url[PATH_MAX];
-	int r, update_error = 0, index_fd = AT_FDCWD;
-
-	r = apk_repo_index_url(db, repo, &index_fd, index_url, sizeof index_url, &urlp);
-	if (r < 0) goto err;
+	char cache_url[NAME_MAX], *open_url = repo->url_index.ptr;
+	int r, update_error = 0, open_fd = AT_FDCWD;
 
 	error_action = "opening";
 	if (!(db->ctx->flags & APK_NO_NETWORK)) available_repos = repo_mask;
 	if (repo->is_remote) {
 		if (db->ctx->flags & APK_NO_CACHE) {
 			error_action = "fetching";
-			apk_out_progress_note(out, "fetch " URL_FMT, URL_PRINTF(urlp));
+			apk_out_progress_note(out, "fetch " BLOB_FMT, BLOB_PRINTF(repo->url_index_printable));
 		} else {
 			error_action = "opening from cache";
 			if (repo->stale) {
@@ -1543,27 +1552,27 @@ static void open_repository(struct apk_database *db, int repo_num)
 					break;
 				}
 			}
-			r = apk_repo_index_cache_url(db, repo, &index_fd, index_url, sizeof index_url);
+			open_url = cache_url;
+			r = apk_repo_index_cache_url(db, repo, &open_fd, cache_url, sizeof cache_url);
 			if (r < 0) goto err;
 		}
-	} else if (strncmp(repo->url, "file://localhost/", 17) != 0) {
+	} else if (!apk_blob_starts_with(repo->url_base, APK_BLOB_STRLIT("file://localhost/"))) {
 		available_repos = repo_mask;
 		db->local_repos |= repo_mask;
 	}
-	r = load_index(db, apk_istream_from_fd_url(index_fd, index_url, apk_db_url_since(db, 0)), repo_num);
+	r = load_index(db, apk_istream_from_fd_url(open_fd, open_url, apk_db_url_since(db, 0)), repo_num);
 err:
 	if (r || update_error) {
 		if (repo->is_remote) {
 			if (r) db->repositories.unavailable++;
 			else db->repositories.stale++;
 		}
-		apk_url_parse(&urlp, repo->url);
 		if (update_error)
 			error_action = r ? "updating and opening" : "updating";
 		else
 			update_error = r;
-		apk_warn(out, "%s " URL_FMT ": %s", error_action, URL_PRINTF(urlp),
-			apk_error_str(update_error));
+		apk_warn(out, "%s " BLOB_FMT ": %s",
+			error_action, BLOB_PRINTF(repo->url_index_printable), apk_error_str(update_error));
 	}
 	if (r == 0) {
 		db->available_repos |= available_repos;
@@ -1593,18 +1602,11 @@ static int add_repos_from_file(void *ctx, int dirfd, const char *file)
 	return 0;
 }
 
-static void apk_db_setup_repositories(struct apk_database *db, const char *cache_dir)
+static void apk_db_setup_repositories(struct apk_database *db, apk_blob_t cache_dir)
 {
-	/* This is the SHA-1 of the string 'cache'. Repo hashes like this
-	 * are truncated to APK_CACHE_CSUM_BYTES and always use SHA-1. */
 	db->repos[APK_REPOSITORY_CACHED] = (struct apk_repository) {
-		.hash.data = {
-			0xb0,0x35,0x92,0x80,0x6e,0xfa,0xbf,0xee,0xb7,0x09,
-			0xf5,0xa7,0x0a,0x7c,0x17,0x26,0x69,0xb0,0x05,0x38 },
-		.hash.len = APK_DIGEST_LENGTH_SHA1,
-		.hash.alg = APK_DIGEST_SHA1,
-		.url = cache_dir,
-		.url_base = APK_BLOB_STR(cache_dir),
+		.url_base = cache_dir,
+		.url_base_printable = cache_dir,
 		.pkgname_spec = db->ctx->default_cachename_spec,
 	};
 	db->num_repos = APK_REPOSITORY_FIRST_CONFIGURED;
@@ -1940,7 +1942,7 @@ int apk_db_open(struct apk_database *db, struct apk_ctx *ac)
 	    !(ac->flags & APK_NO_NETWORK))
 		db->autoupdate = 1;
 
-	apk_db_setup_repositories(db, ac->cache_dir);
+	apk_db_setup_repositories(db, APK_BLOB_STR(ac->cache_dir));
 	db->root_fd = apk_ctx_fd_root(ac);
 	db->cache_fd = -APKE_CACHE_NOT_AVAILABLE;
 	db->permanent = !detect_tmpfs_root(db);
@@ -2249,8 +2251,6 @@ void apk_db_close(struct apk_database *db)
 		apk_pkg_uninstall(NULL, ipkg->pkg);
 	}
 
-	for (int i = APK_REPOSITORY_FIRST_CONFIGURED; i < db->num_repos; i++)
-		free((void*) db->repos[i].url);
 	foreach_array_item(ppath, db->protected_paths)
 		free(ppath->relative_pattern);
 	apk_protected_path_array_free(&db->protected_paths);
@@ -3060,7 +3060,7 @@ static int apk_db_unpack_pkg(struct apk_database *db,
 			r = -APKE_PACKAGE_NOT_FOUND;
 			goto err_msg;
 		}
-		r = apk_repo_package_url(db, repo, pkg, &file_fd, file_url, sizeof file_url, NULL);
+		r = apk_repo_package_url(db, repo, pkg, &file_fd, file_url, sizeof file_url);
 		if (r < 0) goto err_msg;
 		if (!(pkg->repos & db->local_repos)) need_copy = TRUE;
 	} else {
@@ -3082,7 +3082,7 @@ static int apk_db_unpack_pkg(struct apk_database *db,
 	is = apk_progress_istream(&pis, is, prog);
 	if (need_copy) {
 		struct apk_istream *origis = is;
-		r = apk_repo_package_url(db, &db->repos[APK_REPOSITORY_CACHED], pkg, &cache_fd, cache_url, sizeof cache_url, NULL);
+		r = apk_repo_package_url(db, &db->repos[APK_REPOSITORY_CACHED], pkg, &cache_fd, cache_url, sizeof cache_url);
 		if (r == 0)
 			is = apk_istream_tee(is, apk_ostream_to_file(cache_fd, cache_url, 0644),
 				APK_ISTREAM_TEE_COPY_META|APK_ISTREAM_TEE_OPTIONAL);
diff --git a/src/print.c b/src/print.c
index b5e68c97..8814fe74 100644
--- a/src/print.c
+++ b/src/print.c
@@ -133,28 +133,30 @@ const char *apk_last_path_segment(const char *path)
 	return last == NULL ? path : last + 1;
 }
 
-void apk_url_parse(struct apk_url_print *urlp, const char *url)
+apk_blob_t apk_url_sanitize(apk_blob_t url, struct apk_atom_pool *atoms)
 {
-	const char *authority, *path_or_host, *pw;
-
-	*urlp = (struct apk_url_print) {
-		.url = "",
-		.pwmask = "",
-		.url_or_host = url,
-	};
-
-	if (!(authority = strstr(url, "://"))) return;
-	authority += 3;
-	path_or_host = strpbrk(authority, "/@");
-	if (!path_or_host || *path_or_host == '/') return;
-	pw = strpbrk(authority, "@:");
-	if (!pw || *pw == '@') return;
-	*urlp = (struct apk_url_print) {
-		.url = url,
-		.pwmask = "*",
-		.url_or_host = path_or_host,
-		.len_before_pw = pw - url + 1,
-	};
+	char buf[PATH_MAX];
+	int password_start = 0;
+	int authority = apk_blob_contains(url, APK_BLOB_STRLIT("://"));
+	if (authority < 0) return url;
+
+	for (int i = authority + 3; i < url.len; i++) {
+		switch (url.ptr[i]) {
+		case '/':
+			return url;
+		case '@':
+			if (!password_start) return url;
+			// password_start ... i-1 is the password
+			return *apk_atomize_dup(atoms,
+				apk_blob_fmt(buf, sizeof buf, "%.*s*%.*s",
+					password_start, url.ptr,
+					(int)(url.len - i), &url.ptr[i]));
+		case ':':
+			if (!password_start) password_start = i + 1;
+			break;
+		}
+	}
+	return url;
 }
 
 void apk_out_reset(struct apk_out *out)
diff --git a/test/unit/apk_test.h b/test/unit/apk_test.h
index 4dc30818..255eb806 100644
--- a/test/unit/apk_test.h
+++ b/test/unit/apk_test.h
@@ -5,6 +5,20 @@
 
 #define assert_ptr_ok(c) _assert_true(!IS_ERR(c), #c, __FILE__, __LINE__)
 
+#define _assert_blob_equal(a, b, file, line) do { \
+		_assert_int_equal(a.len, b.len, file, line); \
+		_assert_memory_equal(a.ptr, b.ptr, a.len, file, line); \
+	} while (0)
+#define assert_blob_equal(a, b) _assert_blob_equal(a, b, __FILE__, __LINE__)
+
+#define _assert_blob_identical(a, b, file, line) do { \
+		_assert_int_equal(a.len, b.len, file, line); \
+		_assert_int_equal(cast_ptr_to_largest_integral_type(a.ptr), \
+				  cast_ptr_to_largest_integral_type(b.ptr), \
+				  file, line); \
+	} while (0)
+#define assert_blob_identical(a, b) _assert_blob_identical(a, b, __FILE__, __LINE__)
+
 void test_register(const char *, UnitTestFunction);
 
 #define APK_TEST(test_name) \
diff --git a/test/unit/blob_test.c b/test/unit/blob_test.c
index 80c418ea..2afce2bb 100644
--- a/test/unit/blob_test.c
+++ b/test/unit/blob_test.c
@@ -1,5 +1,7 @@
 #include "apk_test.h"
 #include "apk_blob.h"
+#include "apk_atom.h"
+#include "apk_print.h"
 
 APK_TEST(blob_foreach_word_test) {
 	int ch = 'a';
@@ -24,3 +26,23 @@ APK_TEST(blob_split) {
 	assert_int_equal(0, apk_blob_compare(l, APK_BLOB_STRLIT("bar")));
 	assert_int_equal(0, apk_blob_compare(r, APK_BLOB_STRLIT("foo")));
 }
+
+APK_TEST(blob_url_sanitize) {
+	struct {
+		const char *url, *sanitized;
+	} tests[] = {
+		{ "http://example.com", NULL },
+		{ "http://foo@example.com", NULL },
+		{ "http://foo:pass@example.com", "http://foo:*@example.com" },
+		{ "http://example.com/foo:pass@bar", NULL },
+	};
+	struct apk_atom_pool atoms;
+	apk_atom_init(&atoms);
+	for (int i = 0; i < ARRAY_SIZE(tests); i++) {
+		apk_blob_t url = APK_BLOB_STR(tests[i].url);
+		apk_blob_t res = apk_url_sanitize(APK_BLOB_STR(tests[i].url), &atoms);
+		if (tests[i].sanitized) assert_blob_equal(APK_BLOB_STR(tests[i].sanitized), res);
+		else assert_blob_identical(url, res);
+	}
+	apk_atom_free(&atoms);
+}
diff --git a/test/unit/db_test.c b/test/unit/db_test.c
new file mode 100644
index 00000000..4c6062c3
--- /dev/null
+++ b/test/unit/db_test.c
@@ -0,0 +1,27 @@
+#include "apk_test.h"
+#include "apk_database.h"
+
+static void _assert_repoline(apk_blob_t line, apk_blob_t tag, unsigned int type, apk_blob_t url, const char *const file, int lineno)
+{
+	struct apk_repoline rl;
+
+	_assert_true(apk_repo_parse_line(line, &rl), "", file, lineno);
+	_assert_blob_equal(tag, rl.tag, file, lineno);
+	_assert_int_equal(type, rl.type, file, lineno);
+	_assert_blob_equal(url, rl.url, file, lineno);
+}
+#define assert_repoline(line, tag, type, url) _assert_repoline(line, tag, type, url, __FILE__, __LINE__)
+
+APK_TEST(db_repo_parse) {
+	struct apk_repoline rl;
+	apk_blob_t tag = APK_BLOB_STRLIT("@tag");
+	apk_blob_t url = APK_BLOB_STRLIT("http://example.com");
+	apk_blob_t index = APK_BLOB_STRLIT("http://example.com/index.adb");
+
+	assert_repoline(url, APK_BLOB_NULL, APK_REPOTYPE_V2, url);
+	assert_repoline(APK_BLOB_STRLIT("@tag http://example.com"), tag, APK_REPOTYPE_V2, url);
+	assert_repoline(APK_BLOB_STRLIT("http://example.com/index.adb"), APK_BLOB_NULL, APK_REPOTYPE_NDX, index);
+
+	assert_false(apk_repo_parse_line(APK_BLOB_STRLIT("http://example.com extra"), &rl));
+	assert_false(apk_repo_parse_line(APK_BLOB_STRLIT("@tag v3 http://example.com extra"), &rl));
+}
diff --git a/test/unit/meson.build b/test/unit/meson.build
index f7fc3863..8481807c 100644
--- a/test/unit/meson.build
+++ b/test/unit/meson.build
@@ -4,6 +4,7 @@ if cmocka_dep.found()
 
 unit_test_src = [
 	'blob_test.c',
+	'db_test.c',
 	'package_test.c',
 	'process_test.c',
 	'version_test.c',
-- 
GitLab