[FIX] cache qemu : réunir les préfixes de miroir sous une seule clé

Un miroir préfixe le chemin à sa guise — « /rocky/10.2/… »,
« /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… » — et le chemin
entier donnait deux clés pour un même octet : le cache retéléchargeait ce
qu'il détenait. Relevé sur un journal de 7099 noms livrés : 1124 vivaient sous
plusieurs chemins, et 3,18 Gio repartaient à l'amont pour rien. Seuls les six
derniers segments comptent désormais, les segments vides tombant avec eux. Six
est le plus petit nombre sans collision : un chemin Debian en porte six, si
bien que cinq confondraient son paquet avec celui d'Ubuntu, qui porte le même
nom pour d'autres octets — 38 cas mesurés. Vérifié : deux mutations attrapées.

--- EN ---

A mirror prefixes the path as it pleases, and the whole path gave two keys for
one byte: the cache re-downloaded what it held. Measured over a log of 7099
delivered names: 1124 lived under several paths, and 3.18 GiB went back
upstream for nothing. Only the last six segments now count, empty ones falling
with them. Six is the smallest number without collision: a Debian path carries
six, so five would confuse its package with Ubuntu's, which bears the same
name for other bytes — 38 measured cases. Checked: two mutations caught.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-09-17 09:26:56 -04:00
parent 9445734d4f
commit 755e2b49bd
3 changed files with 69 additions and 2 deletions

View file

@ -239,6 +239,46 @@ func TestLesMetadonneesRPMParEmpreinteSontImmuables(t *testing.T) {
}
}
// Un miroir préfixe le chemin à sa guise, et le chemin entier donnait deux
// clés pour un même octet. Relevé sur un journal réel : 1124 noms de paquets
// vivaient sous plusieurs chemins, et 3,18 Gio repartaient à l'amont pour des
// fichiers que le magasin détenait déjà.
func TestLesPrefixesDeMiroirNeFontQuUneCle(t *testing.T) {
for _, cas := range [][2]string{
{
"https://a.example/rocky/10.2/AppStream/x86_64/os/Packages/r/rust-1.92.0-2.el10_2.x86_64.rpm",
"https://b.example/mirror/rocky-linux/10.2/AppStream/x86_64/os/Packages/r/rust-1.92.0-2.el10_2.x86_64.rpm",
},
{
"https://a.example/pub/archive/fedora/linux/updates/42/Everything/x86_64/Packages/n/nodejs-libs-22.22.0-2.fc42.x86_64.rpm",
"https://b.example/pub/fedora-archive/fedora/linux/updates/42/Everything/x86_64/Packages/n/nodejs-libs-22.22.0-2.fc42.x86_64.rpm",
},
{
"https://a.example/pub/rocky//10.2/BaseOS/x86_64/os/Packages/a/avahi-0.9-2.el10.x86_64.rpm",
"https://a.example/pub/rocky/10.2/BaseOS/x86_64/os/Packages/a/avahi-0.9-2.el10.x86_64.rpm",
},
} {
a, _ := url.Parse(cas[0])
b, _ := url.Parse(cas[1])
if KeySansHote("GET", a) != KeySansHote("GET", b) {
t.Errorf("deux chemins du même fichier donnent deux clés :\n %s\n %s",
cas[0], cas[1])
}
}
}
// Deux distributions publient le même NOM pour d'autres octets : les confondre
// servirait le paquet de l'une à l'autre. Six segments gardent ce qui les
// sépare, cinq l'effaceraient — 38 noms en collision sur le journal relevé.
func TestDeuxDistributionsNePartagentPasLaCle(t *testing.T) {
deb, _ := url.Parse("https://deb.example/debian/pool/main/p/poppler-data/poppler-data_0.4.12-1_all.deb")
ubu, _ := url.Parse("https://ubu.example/ubuntu/pool/main/p/poppler-data/poppler-data_0.4.12-1_all.deb")
if KeySansHote("GET", deb) == KeySansHote("GET", ubu) {
t.Error("deux distributions partagent une clé : l'une serait servie" +
" avec le paquet de l'autre")
}
}
// Le même index par empreinte, servi par deux miroirs. Sans clé portable,
// changer de miroir vide le cache de ses index : une installation hors ligne
// échoue alors sur des octets que le magasin détient pourtant, et le message

View file

@ -25,7 +25,7 @@ import (
"time"
)
const version = "0.2.13"
const version = "0.2.14"
func main() {
var (

View file

@ -95,8 +95,35 @@ func Key(method, rawURL string) string {
// Réservé aux fichiers dont le NOM porte l'identité — paquets, index de
// dépôt. L'appliquer à tout ferait entrer en collision les « /index.html » de
// deux sites sans rapport.
//
// Le chemin ENTIER ne suffisait pas : un miroir le préfixe à sa guise —
// « /rocky/10.2/… », « /mirror/rocky-linux/10.2/… », « /pub/archive/fedora/… »
// — et le même octet prenait alors deux clés. Seule la FIN du chemin est
// retenue (voir SegmentsDeCle), ce qui réunit ces copies sans jamais
// confondre deux distributions.
// SegmentsDeCle : combien de segments de FIN de chemin identifient un fichier.
//
// Six, et pas moins : un chemin Debian en porte exactement six —
// « debian/pool/main/p/<paquet>/<fichier>.deb » — si bien que cinq
// effaceraient le segment de distribution et donneraient la même clé au
// paquet d'Ubuntu, qui porte le même nom pour d'autres octets. Relevé sur un
// journal d'accès de 7099 noms livrés : six réunit 1021 des 1124 noms vus
// sous plusieurs chemins, sans confondre aucun contenu ; cinq en confond 38.
const SegmentsDeCle = 6
func KeySansHote(method string, u *url.URL) string {
chemin := u.Path
segments := make([]string, 0, SegmentsDeCle+2)
for _, s := range strings.Split(u.Path, "/") {
// Les segments vides tombent : un miroir écrit « /pub/rocky//10.2 »,
// et deux écritures d'un même chemin feraient sinon deux clés.
if s != "" {
segments = append(segments, s)
}
}
if len(segments) > SegmentsDeCle {
segments = segments[len(segments)-SegmentsDeCle:]
}
chemin := strings.Join(segments, "/")
if u.RawQuery != "" {
chemin += "?" + u.RawQuery
}