mirror of
https://github.com/moonD4rk/HackBrowserData.git
synced 2026-05-19 18:58:03 +02:00
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
This commit is contained in:
+25
-10
@@ -16,19 +16,30 @@ import (
|
||||
type Browser interface {
|
||||
BrowserName() string
|
||||
ProfileName() string
|
||||
ProfileDir() string
|
||||
Extract(categories []types.Category) (*types.BrowserData, error)
|
||||
}
|
||||
|
||||
// PickBrowsers returns browsers matching the given name.
|
||||
// When name is "all", all known browsers are tried.
|
||||
// profilePath overrides the default user data directory (only when targeting a specific browser).
|
||||
func PickBrowsers(name, profilePath string) ([]Browser, error) {
|
||||
return pickFromConfigs(platformBrowsers(), name, profilePath)
|
||||
// PickOptions configures which browsers to pick.
|
||||
type PickOptions struct {
|
||||
Name string // browser name filter: "all"|"chrome"|"firefox"|...
|
||||
ProfilePath string // custom profile directory override
|
||||
KeychainPassword string // macOS keychain password (ignored on other platforms)
|
||||
}
|
||||
|
||||
// PickBrowsers returns browsers matching the given options.
|
||||
// When Name is "all", all known browsers are tried.
|
||||
// ProfilePath overrides the default user data directory (only when targeting a specific browser).
|
||||
func PickBrowsers(opts PickOptions) ([]Browser, error) {
|
||||
return pickFromConfigs(platformBrowsers(), opts)
|
||||
}
|
||||
|
||||
// pickFromConfigs is the testable core of PickBrowsers.
|
||||
func pickFromConfigs(configs []types.BrowserConfig, name, profilePath string) ([]Browser, error) {
|
||||
name = strings.ToLower(name)
|
||||
func pickFromConfigs(configs []types.BrowserConfig, opts PickOptions) ([]Browser, error) {
|
||||
name := strings.ToLower(opts.Name)
|
||||
if name == "" {
|
||||
name = "all"
|
||||
}
|
||||
|
||||
var browsers []Browser
|
||||
for _, cfg := range configs {
|
||||
@@ -36,14 +47,18 @@ func pickFromConfigs(configs []types.BrowserConfig, name, profilePath string) ([
|
||||
continue
|
||||
}
|
||||
|
||||
if profilePath != "" && name != "all" {
|
||||
if opts.ProfilePath != "" && name != "all" {
|
||||
if cfg.Kind == types.KindFirefox {
|
||||
cfg.UserDataDir = filepath.Dir(filepath.Clean(profilePath))
|
||||
cfg.UserDataDir = filepath.Dir(filepath.Clean(opts.ProfilePath))
|
||||
} else {
|
||||
cfg.UserDataDir = profilePath
|
||||
cfg.UserDataDir = opts.ProfilePath
|
||||
}
|
||||
}
|
||||
|
||||
if opts.KeychainPassword != "" {
|
||||
cfg.KeychainPassword = opts.KeychainPassword
|
||||
}
|
||||
|
||||
bs, err := newBrowsers(cfg)
|
||||
if err != nil {
|
||||
log.Errorf("browser %s: %v", cfg.Name, err)
|
||||
|
||||
@@ -27,6 +27,7 @@ func TestListBrowsers(t *testing.T) {
|
||||
|
||||
func TestPickFromConfigs_NameFilter(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mkFile(t, dir, "Default", "Preferences")
|
||||
mkFile(t, dir, "Default", "Login Data")
|
||||
mkFile(t, dir, "Default", "History")
|
||||
|
||||
@@ -67,7 +68,7 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
browsers, err := pickFromConfigs(configs, tt.pickName, "")
|
||||
browsers, err := pickFromConfigs(configs, PickOptions{Name: tt.pickName})
|
||||
require.NoError(t, err)
|
||||
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
|
||||
})
|
||||
@@ -76,8 +77,10 @@ func TestPickFromConfigs_NameFilter(t *testing.T) {
|
||||
|
||||
func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
||||
chromeDir := t.TempDir()
|
||||
mkFile(t, chromeDir, "Default", "Preferences")
|
||||
mkFile(t, chromeDir, "Default", "Login Data")
|
||||
mkFile(t, chromeDir, "Default", "History")
|
||||
mkFile(t, chromeDir, "Profile 1", "Preferences")
|
||||
mkFile(t, chromeDir, "Profile 1", "Login Data")
|
||||
mkFile(t, chromeDir, "Profile 1", "History")
|
||||
|
||||
@@ -86,6 +89,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
||||
mkFile(t, firefoxDir, "abc123.default-release", "places.sqlite")
|
||||
|
||||
yandexDir := t.TempDir()
|
||||
mkFile(t, yandexDir, "Default", "Preferences")
|
||||
mkFile(t, yandexDir, "Default", "Ya Passman Data")
|
||||
mkFile(t, yandexDir, "Default", "History")
|
||||
|
||||
@@ -129,7 +133,7 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
browsers, err := pickFromConfigs(tt.configs, "all", "")
|
||||
browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: "all"})
|
||||
require.NoError(t, err)
|
||||
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
|
||||
})
|
||||
@@ -138,8 +142,10 @@ func TestPickFromConfigs_BrowserKind(t *testing.T) {
|
||||
|
||||
func TestPickFromConfigs_ProfilePath(t *testing.T) {
|
||||
chromeDir := t.TempDir()
|
||||
mkFile(t, chromeDir, "Default", "Preferences")
|
||||
mkFile(t, chromeDir, "Default", "Login Data")
|
||||
mkFile(t, chromeDir, "Default", "History")
|
||||
mkFile(t, chromeDir, "Profile 1", "Preferences")
|
||||
mkFile(t, chromeDir, "Profile 1", "Login Data")
|
||||
mkFile(t, chromeDir, "Profile 1", "History")
|
||||
|
||||
@@ -189,7 +195,7 @@ func TestPickFromConfigs_ProfilePath(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
browsers, err := pickFromConfigs(tt.configs, tt.pickName, tt.profilePath)
|
||||
browsers, err := pickFromConfigs(tt.configs, PickOptions{Name: tt.pickName, ProfilePath: tt.profilePath})
|
||||
require.NoError(t, err)
|
||||
assertBrowsers(t, browsers, tt.wantNames, tt.wantProfiles)
|
||||
})
|
||||
|
||||
@@ -68,6 +68,12 @@ func platformBrowsers() []types.BrowserConfig {
|
||||
Kind: types.KindChromiumYandex,
|
||||
UserDataDir: homeDir + "/AppData/Local/Yandex/YandexBrowser/User Data",
|
||||
},
|
||||
{
|
||||
Key: "360x",
|
||||
Name: speed360XName,
|
||||
Kind: types.KindChromium,
|
||||
UserDataDir: homeDir + "/AppData/Local/360ChromeX/Chrome/User Data",
|
||||
},
|
||||
{
|
||||
Key: "360",
|
||||
Name: speed360Name,
|
||||
@@ -90,7 +96,7 @@ func platformBrowsers() []types.BrowserConfig {
|
||||
Key: "sogou",
|
||||
Name: sogouName,
|
||||
Kind: types.KindChromium,
|
||||
UserDataDir: homeDir + "/AppData/Roaming/SogouExplorer/Webkit",
|
||||
UserDataDir: homeDir + "/AppData/Local/Sogou/SogouExplorer/User Data",
|
||||
},
|
||||
{
|
||||
Key: "firefox",
|
||||
|
||||
@@ -57,6 +57,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileDir() string { return b.profileDir }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
@@ -173,8 +174,9 @@ func (b *Browser) extractCategory(data *types.BrowserData, cat types.Category, m
|
||||
}
|
||||
}
|
||||
|
||||
// discoverProfiles lists subdirectories of userDataDir that contain at least
|
||||
// one known data source. Each such directory is a browser profile.
|
||||
// discoverProfiles lists subdirectories of userDataDir that are valid
|
||||
// Chromium profile directories. A directory is considered a profile if it
|
||||
// contains a "Preferences" file, which Chromium creates for every profile.
|
||||
func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePath) []string {
|
||||
entries, err := os.ReadDir(userDataDir)
|
||||
if err != nil {
|
||||
@@ -188,18 +190,41 @@ func discoverProfiles(userDataDir string, sources map[types.Category][]sourcePat
|
||||
continue
|
||||
}
|
||||
dir := filepath.Join(userDataDir, e.Name())
|
||||
if hasAnySource(sources, dir) {
|
||||
if isProfileDir(dir) {
|
||||
profiles = append(profiles, dir)
|
||||
}
|
||||
}
|
||||
|
||||
// Flat layout fallback (older Opera): data files directly in userDataDir
|
||||
// Flat layout fallback (older Opera): data files directly in userDataDir.
|
||||
// Opera stores data alongside Local State in userDataDir itself, so check
|
||||
// for any known source file instead of Preferences.
|
||||
if len(profiles) == 0 && hasAnySource(sources, userDataDir) {
|
||||
profiles = append(profiles, userDataDir)
|
||||
}
|
||||
return profiles
|
||||
}
|
||||
|
||||
// profileMarkers are filenames that identify a directory as a Chromium profile.
|
||||
// Chromium creates a per-profile preferences file on first use; checking for
|
||||
// its existence filters out non-profile subdirectories (Crashpad, ShaderCache, etc.).
|
||||
//
|
||||
// - "Preferences" — standard Chromium and all major forks (Chrome, Edge, Brave, …)
|
||||
// - "Preferences_02" — Tencent-based browsers (QQ Browser, Sogou Explorer)
|
||||
var profileMarkers = []string{
|
||||
"Preferences",
|
||||
"Preferences_02",
|
||||
}
|
||||
|
||||
// isProfileDir reports whether dir is a valid Chromium profile directory.
|
||||
func isProfileDir(dir string) bool {
|
||||
for _, name := range profileMarkers {
|
||||
if _, err := os.Stat(filepath.Join(dir, name)); err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasAnySource checks if dir contains at least one source file or directory.
|
||||
func hasAnySource(sources map[types.Category][]sourcePath, dir string) bool {
|
||||
for _, candidates := range sources {
|
||||
|
||||
@@ -45,6 +45,7 @@ func buildFixtures() {
|
||||
fixture.chrome = filepath.Join(fixture.root, "chrome")
|
||||
mkFile(fixture.chrome, "Local State")
|
||||
for _, p := range []string{"Default", "Profile 1", "Profile 3"} {
|
||||
mkFile(fixture.chrome, p, "Preferences")
|
||||
mkFile(fixture.chrome, p, "Login Data")
|
||||
mkFile(fixture.chrome, p, "History")
|
||||
mkFile(fixture.chrome, p, "Bookmarks")
|
||||
@@ -60,6 +61,7 @@ func buildFixtures() {
|
||||
|
||||
fixture.opera = filepath.Join(fixture.root, "opera")
|
||||
mkFile(fixture.opera, "Local State")
|
||||
mkFile(fixture.opera, "Default", "Preferences")
|
||||
mkFile(fixture.opera, "Default", "Login Data")
|
||||
mkFile(fixture.opera, "Default", "History")
|
||||
mkFile(fixture.opera, "Default", "Bookmarks")
|
||||
@@ -73,6 +75,7 @@ func buildFixtures() {
|
||||
|
||||
fixture.yandex = filepath.Join(fixture.root, "yandex")
|
||||
mkFile(fixture.yandex, "Local State")
|
||||
mkFile(fixture.yandex, "Default", "Preferences")
|
||||
mkFile(fixture.yandex, "Default", "Ya Passman Data")
|
||||
mkFile(fixture.yandex, "Default", "Ya Credit Cards")
|
||||
mkFile(fixture.yandex, "Default", "History")
|
||||
@@ -80,14 +83,17 @@ func buildFixtures() {
|
||||
mkFile(fixture.yandex, "Default", "Bookmarks")
|
||||
|
||||
fixture.oldCookies = filepath.Join(fixture.root, "old-cookies")
|
||||
mkFile(fixture.oldCookies, "Default", "Preferences")
|
||||
mkFile(fixture.oldCookies, "Default", "History")
|
||||
mkFile(fixture.oldCookies, "Default", "Cookies")
|
||||
|
||||
fixture.bothCookies = filepath.Join(fixture.root, "both-cookies")
|
||||
mkFile(fixture.bothCookies, "Default", "Preferences")
|
||||
mkFile(fixture.bothCookies, "Default", "Cookies")
|
||||
mkFile(fixture.bothCookies, "Default", "Network", "Cookies")
|
||||
|
||||
fixture.leveldb = filepath.Join(fixture.root, "leveldb")
|
||||
mkFile(fixture.leveldb, "Default", "Preferences")
|
||||
mkFile(fixture.leveldb, "Default", "History")
|
||||
mkDir(fixture.leveldb, "Default", "Local Storage", "leveldb")
|
||||
mkFile(fixture.leveldb, "Default", "Local Storage", "leveldb", "000001.ldb")
|
||||
@@ -95,6 +101,7 @@ func buildFixtures() {
|
||||
mkFile(fixture.leveldb, "Default", "Session Storage", "000001.ldb")
|
||||
|
||||
fixture.leveldbOnly = filepath.Join(fixture.root, "leveldb-only")
|
||||
mkFile(fixture.leveldbOnly, "Default", "Preferences")
|
||||
mkDir(fixture.leveldbOnly, "Default", "Local Storage", "leveldb")
|
||||
mkDir(fixture.leveldbOnly, "Default", "Session Storage")
|
||||
|
||||
|
||||
+2
-1
@@ -19,7 +19,8 @@ const (
|
||||
coccocName = "CocCoc"
|
||||
yandexName = "Yandex"
|
||||
firefoxName = "Firefox"
|
||||
speed360Name = "360speed"
|
||||
speed360Name = "360 Speed"
|
||||
speed360XName = "360 Speed X"
|
||||
qqBrowserName = "QQ"
|
||||
dcBrowserName = "DC"
|
||||
sogouName = "Sogou"
|
||||
|
||||
@@ -47,6 +47,7 @@ func NewBrowsers(cfg types.BrowserConfig) ([]*Browser, error) {
|
||||
}
|
||||
|
||||
func (b *Browser) BrowserName() string { return b.cfg.Name }
|
||||
func (b *Browser) ProfileDir() string { return b.profileDir }
|
||||
func (b *Browser) ProfileName() string {
|
||||
if b.profileDir == "" {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user