Skip to content

Commit 4af2ded

Browse files
authored
feat: cli migrate to cobra with subcommands (#550)
* feat: migrate CLI to cobra with dump/list/version subcommands (#546) * fix: remove residual duckduckgo references and add README/LICENSE to release archives * fix: address PR review feedback from Copilot
1 parent 068b821 commit 4af2ded

15 files changed

Lines changed: 414 additions & 108 deletions

File tree

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ linters:
8383
min-len: 2
8484
min-occurrences: 3
8585
ignore-string-values:
86+
- "all"
8687
- "csv"
8788
- "json"
8889
gocritic:

.goreleaser.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ before:
66

77
builds:
88
- id: "hack-browser-data"
9-
main: ./cmd/hack-browser-data/main.go
9+
main: ./cmd/hack-browser-data/
1010
binary: hack-browser-data
1111
env:
1212
- CGO_ENABLED=0
@@ -23,11 +23,17 @@ builds:
2323
- -trimpath
2424
ldflags:
2525
- -s -w
26+
- -X main.version={{.Version}}
27+
- -X main.commit={{.ShortCommit}}
28+
- -X main.buildDate={{.Date}}
2629

2730
archives:
2831
- id: "archive"
29-
format: zip
30-
builds: ["hack-browser-data"]
32+
formats:
33+
- zip
34+
files:
35+
- README.md
36+
- LICENSE
3137
name_template: >-
3238
hack-browser-data-
3339
{{- if eq .Os "darwin" }}osx

browser/browser.go

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,49 @@ import (
1616
type Browser interface {
1717
BrowserName() string
1818
ProfileName() string
19+
ProfileDir() string
1920
Extract(categories []types.Category) (*types.BrowserData, error)
2021
}
2122

22-
// PickBrowsers returns browsers matching the given name.
23-
// When name is "all", all known browsers are tried.
24-
// profilePath overrides the default user data directory (only when targeting a specific browser).
25-
func PickBrowsers(name, profilePath string) ([]Browser, error) {
26-
return pickFromConfigs(platformBrowsers(), name, profilePath)
23+
// PickOptions configures which browsers to pick.
24+
type PickOptions struct {
25+
Name string // browser name filter: "all"|"chrome"|"firefox"|...
26+
ProfilePath string // custom profile directory override
27+
KeychainPassword string // macOS keychain password (ignored on other platforms)
28+
}
29+
30+
// PickBrowsers returns browsers matching the given options.
31+
// When Name is "all", all known browsers are tried.
32+
// ProfilePath overrides the default user data directory (only when targeting a specific browser).
33+
func PickBrowsers(opts PickOptions) ([]Browser, error) {
34+
return pickFromConfigs(platformBrowsers(), opts)
2735
}
2836

2937
// pickFromConfigs is the testable core of PickBrowsers.
30-
func pickFromConfigs(configs []types.BrowserConfig, name, profilePath string) ([]Browser, error) {
31-
name = strings.ToLower(name)
38+
func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser, error) {
39+
name := strings.ToLower(opts.Name)
40+
if name == "" {
41+
name = "all"
42+
}
3243

3344
var browsers []Browser
3445
for _, cfg := range configs {
3546
if name != "all" && cfg.Key != name {
3647
continue
3748
}
3849

39-
if profilePath != "" && name != "all" {
50+
if opts.ProfilePath != "" && name != "all" {
4051
if cfg.Kind == types.KindFirefox {
41-
cfg.UserDataDir = filepath.Dir(filepath.Clean(profilePath))
52+
cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath))
4253
} else {
43-
cfg.UserDataDir = profilePath
54+
cfg.UserDataDir = opts.ProfilePath
4455
}
4556
}
4657

58+
if opts.KeychainPassword != "" {
59+
cfg.KeychainPassword = opts.KeychainPassword
60+
}
61+
4762
bs, err := newBrowsers(cfg)
4863
if err != nil {
4964
log.Errorf("browser %s: %v", cfg.Name, err)

browser/browser_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func TestListBrowsers(t *testing.T) {
2727

2828
func TestPickFromConfigs_NameFilter(t *testing.T) {
2929
dir := t.TempDir()
30+
mkFile(t, dir, "Default", "Preferences")
3031
mkFile(t, dir, "Default", "Login Data")
3132
mkFile(t, dir, "Default", "History")
3233

@@ -67,7 +68,7 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
6768

6869
for _, tt := range tests {
6970
t.Run(tt.name, func(t *testing.T) {
70-
browsers, err := pickFromConfigs(configs, tt.pickName, "")
71+
browsers, err := pickFromConfigs(configs, PickOptions{Name: tt.pickName})
7172
require.NoError(t, err)
7273
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
7374
})
@@ -76,8 +77,10 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
7677

7778
func TestPickFromConfigs_BrowserKind(t *testing.T) {
7879
chromeDir := t.TempDir()
80+
mkFile(t, chromeDir, "Default", "Preferences")
7981
mkFile(t, chromeDir, "Default", "Login Data")
8082
mkFile(t, chromeDir, "Default", "History")
83+
mkFile(t, chromeDir, "Profile 1", "Preferences")
8184
mkFile(t, chromeDir, "Profile 1", "Login Data")
8285
mkFile(t, chromeDir, "Profile 1", "History")
8386

@@ -86,6 +89,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
8689
mkFile(t, firefoxDir, "abc123.default-release", "places.sqlite")
8790

8891
yandexDir := t.TempDir()
92+
mkFile(t, yandexDir, "Default", "Preferences")
8993
mkFile(t, yandexDir, "Default", "Ya Passman Data")
9094
mkFile(t, yandexDir, "Default", "History")
9195

@@ -129,7 +133,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
129133

130134
for _, tt := range tests {
131135
t.Run(tt.name, func(t *testing.T) {
132-
browsers, err := pickFromConfigs(tt.configs, "all", "")
136+
browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: "all"})
133137
require.NoError(t, err)
134138
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
135139
})
@@ -138,8 +142,10 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
138142

139143
func TestPickFromConfigs_ProfilePath(t *testing.T) {
140144
chromeDir := t.TempDir()
145+
mkFile(t, chromeDir, "Default", "Preferences")
141146
mkFile(t, chromeDir, "Default", "Login Data")
142147
mkFile(t, chromeDir, "Default", "History")
148+
mkFile(t, chromeDir, "Profile 1", "Preferences")
143149
mkFile(t, chromeDir, "Profile 1", "Login Data")
144150
mkFile(t, chromeDir, "Profile 1", "History")
145151

@@ -189,7 +195,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) {
189195

190196
for _, tt := range tests {
191197
t.Run(tt.name, func(t *testing.T) {
192-
browsers, err := pickFromConfigs(tt.configs, tt.pickName, tt.profilePath)
198+
browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: tt.pickName, ProfilePath: tt.profilePath})
193199
require.NoError(t, err)
194200
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
195201
})

browser/browser_windows.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ func platformBrowsers() []types.BrowserConfig {
6868
Kind: types.KindChromiumYandex,
6969
UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data",
7070
},
71+
{
72+
Key: "360x",
73+
Name: speed360XName,
74+
Kind: types.KindChromium,
75+
UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data",
76+
},
7177
{
7278
Key: "360",
7379
Name: speed360Name,
@@ -90,7 +96,7 @@ func platformBrowsers() []types.BrowserConfig {
9096
Key: "sogou",
9197
Name: sogouName,
9298
Kind: types.KindChromium,
93-
UserDataDir: homeDir + "/AppData/Roaming/SogouExplorer/Webkit",
99+
UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data",
94100
},
95101
{
96102
Key: "firefox",

browser/chromium/chromium.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
5757
}
5858

5959
func (b *Browser) BrowserName() string { return b.cfg.Name }
60+
func (b *Browser) ProfileDir() string { return b.profileDir }
6061
func (b *Browser) ProfileName() string {
6162
if b.profileDir == "" {
6263
return ""
@@ -173,8 +174,9 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
173174
}
174175
}
175176

176-
// discoverProfiles lists subdirectories of userDataDir that contain at least
177-
// one known data source. Each such directory is a browser profile.
177+
// discoverProfiles lists subdirectories of userDataDir that are valid
178+
// Chromium profile directories. A directory is considered a profile if it
179+
// contains a "Preferences" file, which Chromium creates for every profile.
178180
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
179181
entries, err := os.ReadDir(userDataDir)
180182
if err != nil {
@@ -188,18 +190,41 @@ func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePat
188190
continue
189191
}
190192
dir := filepath.Join(userDataDir, e.Name())
191-
if hasAnySource(sources, dir) {
193+
if isProfileDir(dir) {
192194
profiles = append(profiles, dir)
193195
}
194196
}
195197

196-
// Flat layout fallback (older Opera): data files directly in userDataDir
198+
// Flat layout fallback (older Opera): data files directly in userDataDir.
199+
// Opera stores data alongside Local State in userDataDir itself, so check
200+
// for any known source file instead of Preferences.
197201
if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
198202
profiles = append(profiles, userDataDir)
199203
}
200204
return profiles
201205
}
202206

207+
// profileMarkers are filenames that identify a directory as a Chromium profile.
208+
// Chromium creates a per-profile preferences file on first use; checking for
209+
// its existence filters out non-profile subdirectories (Crashpad, ShaderCache, etc.).
210+
//
211+
// - "Preferences" — standard Chromium and all major forks (Chrome, Edge, Brave, …)
212+
// - "Preferences_02" — Tencent-based browsers (QQ Browser, Sogou Explorer)
213+
var profileMarkers = []string{
214+
"Preferences",
215+
"Preferences_02",
216+
}
217+
218+
// isProfileDir reports whether dir is a valid Chromium profile directory.
219+
func isProfileDir(dir string) bool {
220+
for _, name := range profileMarkers {
221+
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
222+
return true
223+
}
224+
}
225+
return false
226+
}
227+
203228
// hasAnySource checks if dir contains at least one source file or directory.
204229
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
205230
for _, candidates := range sources {

browser/chromium/chromium_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func buildFixtures() {
4545
fixture.chrome = filepath.Join(fixture.root, "chrome")
4646
mkFile(fixture.chrome, "Local State")
4747
for _, p := range []string{"Default", "Profile 1", "Profile 3"} {
48+
mkFile(fixture.chrome, p, "Preferences")
4849
mkFile(fixture.chrome, p, "Login Data")
4950
mkFile(fixture.chrome, p, "History")
5051
mkFile(fixture.chrome, p, "Bookmarks")
@@ -60,6 +61,7 @@ func buildFixtures() {
6061

6162
fixture.opera = filepath.Join(fixture.root, "opera")
6263
mkFile(fixture.opera, "Local State")
64+
mkFile(fixture.opera, "Default", "Preferences")
6365
mkFile(fixture.opera, "Default", "Login Data")
6466
mkFile(fixture.opera, "Default", "History")
6567
mkFile(fixture.opera, "Default", "Bookmarks")
@@ -73,28 +75,33 @@ func buildFixtures() {
7375

7476
fixture.yandex = filepath.Join(fixture.root, "yandex")
7577
mkFile(fixture.yandex, "Local State")
78+
mkFile(fixture.yandex, "Default", "Preferences")
7679
mkFile(fixture.yandex, "Default", "Ya Passman Data")
7780
mkFile(fixture.yandex, "Default", "Ya Credit Cards")
7881
mkFile(fixture.yandex, "Default", "History")
7982
mkFile(fixture.yandex, "Default", "Network", "Cookies")
8083
mkFile(fixture.yandex, "Default", "Bookmarks")
8184

8285
fixture.oldCookies = filepath.Join(fixture.root, "old-cookies")
86+
mkFile(fixture.oldCookies, "Default", "Preferences")
8387
mkFile(fixture.oldCookies, "Default", "History")
8488
mkFile(fixture.oldCookies, "Default", "Cookies")
8589

8690
fixture.bothCookies = filepath.Join(fixture.root, "both-cookies")
91+
mkFile(fixture.bothCookies, "Default", "Preferences")
8792
mkFile(fixture.bothCookies, "Default", "Cookies")
8893
mkFile(fixture.bothCookies, "Default", "Network", "Cookies")
8994

9095
fixture.leveldb = filepath.Join(fixture.root, "leveldb")
96+
mkFile(fixture.leveldb, "Default", "Preferences")
9197
mkFile(fixture.leveldb, "Default", "History")
9298
mkDir(fixture.leveldb, "Default", "Local Storage", "leveldb")
9399
mkFile(fixture.leveldb, "Default", "Local Storage", "leveldb", "000001.ldb")
94100
mkDir(fixture.leveldb, "Default", "Session Storage")
95101
mkFile(fixture.leveldb, "Default", "Session Storage", "000001.ldb")
96102

97103
fixture.leveldbOnly = filepath.Join(fixture.root, "leveldb-only")
104+
mkFile(fixture.leveldbOnly, "Default", "Preferences")
98105
mkDir(fixture.leveldbOnly, "Default", "Local Storage", "leveldb")
99106
mkDir(fixture.leveldbOnly, "Default", "Session Storage")
100107

browser/consts.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const (
1919
coccocName = "CocCoc"
2020
yandexName = "Yandex"
2121
firefoxName = "Firefox"
22-
speed360Name = "360speed"
22+
speed360Name = "360 Speed"
23+
speed360XName = "360 Speed X"
2324
qqBrowserName = "QQ"
2425
dcBrowserName = "DC"
2526
sogouName = "Sogou"

browser/firefox/firefox.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
4747
}
4848

4949
func (b *Browser) BrowserName() string { return b.cfg.Name }
50+
func (b *Browser) ProfileDir() string { return b.profileDir }
5051
func (b *Browser) ProfileName() string {
5152
if b.profileDir == "" {
5253
return ""

0 commit comments

Comments
 (0)