fix(favicon): address PR review cache and href issues
- commit
- 8305e33e4f895eeefe1b702eff85aaacd3fde1cc
- author
- brice · · 2026-06-07 18:39 UTC
- parent
- 8a0ce09
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +51 -4
M internal/infrastructure/cef/favicon_discovery.go +6 -2
| @@ -87,8 +87,12 @@ func faviconRel(rel string) bool { | |||
| 87 | 87 | } | |
| 88 | 88 | ||
| 89 | 89 | func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) { | |
| 90 | - | icon, err := url.Parse(strings.TrimSpace(rawIconURL)) | |
| 91 | - | if err != nil || rawIconURL == "" { | |
| 90 | + | trimmed := strings.TrimSpace(rawIconURL) | |
| 91 | + | if trimmed == "" { | |
| 92 | + | return "", false | |
| 93 | + | } | |
| 94 | + | icon, err := url.Parse(trimmed) | |
| 95 | + | if err != nil { | |
| 92 | 96 | return "", false | |
| 93 | 97 | } | |
| 94 | 98 | if icon.IsAbs() { | |
| @@ -87,8 +87,12 @@ func faviconRel(rel string) bool { | |||||
| 87 | } | 87 | } | ||
| 88 | 88 | ||||
| 89 | func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) { | 89 | func resolveFaviconURL(pageURL, rawIconURL string) (string, bool) { | ||
| 90 | - | icon, err := url.Parse(strings.TrimSpace(rawIconURL)) | 90 | + | trimmed := strings.TrimSpace(rawIconURL) |
| 91 | - | if err != nil || rawIconURL == "" { | 91 | + | if trimmed == "" { |
| 92 | + | return "", false | |||
| 93 | + | } | |||
| 94 | + | icon, err := url.Parse(trimmed) | |||
| 95 | + | if err != nil { | |||
| 92 | return "", false | 96 | return "", false | ||
| 93 | } | 97 | } | ||
| 94 | if icon.IsAbs() { | 98 | if icon.IsAbs() { | ||
M internal/infrastructure/cef/favicon_discovery_test.go +5 -0
| @@ -45,6 +45,11 @@ func TestFaviconDiscoveryIgnoresUnsafeIconURLs(t *testing.T) { | |||
| 45 | 45 | require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got) | |
| 46 | 46 | } | |
| 47 | 47 | ||
| 48 | + | func TestFaviconDiscoveryIgnoresWhitespaceOnlyHrefs(t *testing.T) { | |
| 49 | + | got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href=" ">`) | |
| 50 | + | require.Equal(t, []string{"https://example.com/favicon.ico"}, got) | |
| 51 | + | } | |
| 52 | + | ||
| 48 | 53 | func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) { | |
| 49 | 54 | prevDecode := decodeCEFStringList | |
| 50 | 55 | decodeCEFStringList = func(list purecef.StringList) []string { | |
| @@ -45,6 +45,11 @@ func TestFaviconDiscoveryIgnoresUnsafeIconURLs(t *testing.T) { | |||||
| 45 | require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got) | 45 | require.Equal(t, []string{"https://cdn.example.com/icon.png", "https://example.com/favicon.ico"}, got) | ||
| 46 | } | 46 | } | ||
| 47 | 47 | ||||
| 48 | + | func TestFaviconDiscoveryIgnoresWhitespaceOnlyHrefs(t *testing.T) { | |||
| 49 | + | got := DiscoverFaviconCandidates("https://example.com/path", `<link rel="icon" href=" ">`) | |||
| 50 | + | require.Equal(t, []string{"https://example.com/favicon.ico"}, got) | |||
| 51 | + | } | |||
| 52 | + | ||||
| 48 | func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) { | 53 | func TestCEFOnFaviconUrlchangeForwardsOrderedCandidates(t *testing.T) { | ||
| 49 | prevDecode := decodeCEFStringList | 54 | prevDecode := decodeCEFStringList | ||
| 50 | decodeCEFStringList = func(list purecef.StringList) []string { | 55 | decodeCEFStringList = func(list purecef.StringList) []string { | ||
M internal/ui/adapter/favicon.go +21 -2
| @@ -234,7 +234,7 @@ func (a *FaviconAdapter) resolveTexture(ctx context.Context, pageURL string, sch | |||
| 234 | 234 | } | |
| 235 | 235 | texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes) | |
| 236 | 236 | if texture != nil { | |
| 237 | - | a.setTexture(string(resolved.Key), texture) | |
| 237 | + | a.setResolvedTexture(pageURL, resolved.Key, texture) | |
| 238 | 238 | } | |
| 239 | 239 | return texture | |
| 240 | 240 | } | |
| @@ -512,10 +512,19 @@ func (a *FaviconAdapter) Close() { | |||
| 512 | 512 | } | |
| 513 | 513 | } | |
| 514 | 514 | ||
| 515 | - | // Invalidate clears one resolved-key texture cache entry. | |
| 515 | + | // Invalidate clears one resolved-key texture cache entry and any domain aliases | |
| 516 | + | // pointing at the same texture. | |
| 516 | 517 | func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error { | |
| 517 | 518 | a.mu.Lock() | |
| 519 | + | texture := a.textureCache[string(key)] | |
| 518 | 520 | delete(a.textureCache, string(key)) | |
| 521 | + | if texture != nil { | |
| 522 | + | for cacheKey, cached := range a.textureCache { | |
| 523 | + | if cached == texture { | |
| 524 | + | delete(a.textureCache, cacheKey) | |
| 525 | + | } | |
| 526 | + | } | |
| 527 | + | } | |
| 519 | 528 | a.mu.Unlock() | |
| 520 | 529 | return nil | |
| 521 | 530 | } | |
| @@ -535,6 +544,16 @@ func (a *FaviconAdapter) Size() int { | |||
| 535 | 544 | return size | |
| 536 | 545 | } | |
| 537 | 546 | ||
| 547 | + | // setResolvedTexture stores resolver results under both the resolved favicon key | |
| 548 | + | // and the current page domain. The domain alias keeps UI lookups fast while | |
| 549 | + | // Invalidate removes aliases sharing the same texture during the migration path. | |
| 550 | + | func (a *FaviconAdapter) setResolvedTexture(pageURL string, key favicon.Key, texture *gdk.Texture) { | |
| 551 | + | a.setTexture(string(key), texture) | |
| 552 | + | if domain := domainurl.ExtractDomain(pageURL); domain != "" { | |
| 553 | + | a.setTexture(domain, texture) | |
| 554 | + | } | |
| 555 | + | } | |
| 556 | + | ||
| 538 | 557 | // setTexture stores a texture in the cache. | |
| 539 | 558 | func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) { | |
| 540 | 559 | if domain == "" || texture == nil { | |
| @@ -234,7 +234,7 @@ func (a *FaviconAdapter) resolveTexture(ctx context.Context, pageURL string, sch | |||||
| 234 | } | 234 | } | ||
| 235 | texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes) | 235 | texture := a.textureFromBytesOnGTK(ctx, resolved.Bytes) | ||
| 236 | if texture != nil { | 236 | if texture != nil { | ||
| 237 | - | a.setTexture(string(resolved.Key), texture) | 237 | + | a.setResolvedTexture(pageURL, resolved.Key, texture) |
| 238 | } | 238 | } | ||
| 239 | return texture | 239 | return texture | ||
| 240 | } | 240 | } | ||
| @@ -512,10 +512,19 @@ func (a *FaviconAdapter) Close() { | |||||
| 512 | } | 512 | } | ||
| 513 | } | 513 | } | ||
| 514 | 514 | ||||
| 515 | - | // Invalidate clears one resolved-key texture cache entry. | 515 | + | // Invalidate clears one resolved-key texture cache entry and any domain aliases |
| 516 | + | // pointing at the same texture. | |||
| 516 | func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error { | 517 | func (a *FaviconAdapter) Invalidate(_ context.Context, key favicon.Key) error { | ||
| 517 | a.mu.Lock() | 518 | a.mu.Lock() | ||
| 519 | + | texture := a.textureCache[string(key)] | |||
| 518 | delete(a.textureCache, string(key)) | 520 | delete(a.textureCache, string(key)) | ||
| 521 | + | if texture != nil { | |||
| 522 | + | for cacheKey, cached := range a.textureCache { | |||
| 523 | + | if cached == texture { | |||
| 524 | + | delete(a.textureCache, cacheKey) | |||
| 525 | + | } | |||
| 526 | + | } | |||
| 527 | + | } | |||
| 519 | a.mu.Unlock() | 528 | a.mu.Unlock() | ||
| 520 | return nil | 529 | return nil | ||
| 521 | } | 530 | } | ||
| @@ -535,6 +544,16 @@ func (a *FaviconAdapter) Size() int { | |||||
| 535 | return size | 544 | return size | ||
| 536 | } | 545 | } | ||
| 537 | 546 | ||||
| 547 | + | // setResolvedTexture stores resolver results under both the resolved favicon key | |||
| 548 | + | // and the current page domain. The domain alias keeps UI lookups fast while | |||
| 549 | + | // Invalidate removes aliases sharing the same texture during the migration path. | |||
| 550 | + | func (a *FaviconAdapter) setResolvedTexture(pageURL string, key favicon.Key, texture *gdk.Texture) { | |||
| 551 | + | a.setTexture(string(key), texture) | |||
| 552 | + | if domain := domainurl.ExtractDomain(pageURL); domain != "" { | |||
| 553 | + | a.setTexture(domain, texture) | |||
| 554 | + | } | |||
| 555 | + | } | |||
| 556 | + | ||||
| 538 | // setTexture stores a texture in the cache. | 557 | // setTexture stores a texture in the cache. | ||
| 539 | func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) { | 558 | func (a *FaviconAdapter) setTexture(domain string, texture *gdk.Texture) { | ||
| 540 | if domain == "" || texture == nil { | 559 | if domain == "" || texture == nil { | ||
M internal/ui/adapter/favicon_test.go +19 -0
| @@ -1,12 +1,31 @@ | |||
| 1 | 1 | package adapter | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 4 | 5 | "sync/atomic" | |
| 5 | 6 | "testing" | |
| 6 | 7 | ||
| 8 | + | "github.com/bnema/dumber/internal/domain/favicon" | |
| 9 | + | "github.com/bnema/puregotk/v4/gdk" | |
| 7 | 10 | "github.com/rs/zerolog" | |
| 8 | 11 | ) | |
| 9 | 12 | ||
| 13 | + | func TestFaviconAdapterInvalidateRemovesResolvedTextureAliases(t *testing.T) { | |
| 14 | + | adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{}) | |
| 15 | + | texture := &gdk.Texture{} | |
| 16 | + | adapter.setResolvedTexture("https://docs.example.com/page", favicon.Key("example.com"), texture) | |
| 17 | + | ||
| 18 | + | if adapter.GetTexture("example.com") == nil || adapter.GetTexture("docs.example.com") == nil { | |
| 19 | + | t.Fatal("expected both resolved key and domain alias to be cached") | |
| 20 | + | } | |
| 21 | + | if err := adapter.Invalidate(context.Background(), "example.com"); err != nil { | |
| 22 | + | t.Fatal(err) | |
| 23 | + | } | |
| 24 | + | if adapter.GetTexture("example.com") != nil || adapter.GetTexture("docs.example.com") != nil { | |
| 25 | + | t.Fatal("expected invalidation to remove resolved key and alias") | |
| 26 | + | } | |
| 27 | + | } | |
| 28 | + | ||
| 10 | 29 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | |
| 11 | 30 | adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{}) | |
| 12 | 31 | ||
| @@ -1,12 +1,31 @@ | |||||
| 1 | package adapter | 1 | package adapter | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 4 | "sync/atomic" | 5 | "sync/atomic" | ||
| 5 | "testing" | 6 | "testing" | ||
| 6 | 7 | ||||
| 8 | + | "github.com/bnema/dumber/internal/domain/favicon" | |||
| 9 | + | "github.com/bnema/puregotk/v4/gdk" | |||
| 7 | "github.com/rs/zerolog" | 10 | "github.com/rs/zerolog" | ||
| 8 | ) | 11 | ) | ||
| 9 | 12 | ||||
| 13 | + | func TestFaviconAdapterInvalidateRemovesResolvedTextureAliases(t *testing.T) { | |||
| 14 | + | adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{}) | |||
| 15 | + | texture := &gdk.Texture{} | |||
| 16 | + | adapter.setResolvedTexture("https://docs.example.com/page", favicon.Key("example.com"), texture) | |||
| 17 | + | ||||
| 18 | + | if adapter.GetTexture("example.com") == nil || adapter.GetTexture("docs.example.com") == nil { | |||
| 19 | + | t.Fatal("expected both resolved key and domain alias to be cached") | |||
| 20 | + | } | |||
| 21 | + | if err := adapter.Invalidate(context.Background(), "example.com"); err != nil { | |||
| 22 | + | t.Fatal(err) | |||
| 23 | + | } | |||
| 24 | + | if adapter.GetTexture("example.com") != nil || adapter.GetTexture("docs.example.com") != nil { | |||
| 25 | + | t.Fatal("expected invalidation to remove resolved key and alias") | |||
| 26 | + | } | |||
| 27 | + | } | |||
| 28 | + | ||||
| 10 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | 29 | func TestFaviconWarningDedup_FirstAndRepeated(t *testing.T) { | ||
| 11 | adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{}) | 30 | adapter := NewFaviconAdapter(nil, nil, FaviconAdapterConfig{}) | ||
| 12 | 31 | ||||