package browser import ( "encoding/json" "os" "path/filepath" "sort" ) // ChromeProfile holds metadata about a single Chrome/Chromium profile directory. type ChromeProfile struct { Dir string // directory name (value for --profile-directory), e.g. "Default" Name string // human-readable name from Local State info_cache, e.g. "Personal" Extensions int // number of installed extension dirs under /Extensions (excluding "Temp") HasPreferences bool // true if /Preferences file exists } // localState mirrors the parts of Local State we need. type localState struct { Profile struct { InfoCache map[string]struct { Name string `json:"name"` } `json:"info_cache"` } `json:"profile"` } // ListChromeProfiles scans userDataDir and returns one ChromeProfile per // subdirectory that contains a Preferences file (excluding "System Profile"). // If userDataDir is empty it defaults to ~/.config/chromium. // Names are resolved from Local State; if that file is missing or unparseable // the profile Name field equals Dir. func ListChromeProfiles(userDataDir string) ([]ChromeProfile, error) { if userDataDir == "" { home, err := os.UserHomeDir() if err != nil { return nil, err } userDataDir = filepath.Join(home, ".config", "chromium") } // Parse Local State for human-readable names. Failure is non-fatal. names := make(map[string]string) lsPath := filepath.Join(userDataDir, "Local State") if data, err := os.ReadFile(lsPath); err == nil { var ls localState if json.Unmarshal(data, &ls) == nil { for dir, info := range ls.Profile.InfoCache { names[dir] = info.Name } } } entries, err := os.ReadDir(userDataDir) if err != nil { return nil, err } var profiles []ChromeProfile for _, e := range entries { if !e.IsDir() { continue } dir := e.Name() if dir == "System Profile" { continue } prefPath := filepath.Join(userDataDir, dir, "Preferences") info, err := os.Stat(prefPath) if err != nil || info.IsDir() { continue } // Count extension directories (excluding "Temp"). extCount := 0 extDir := filepath.Join(userDataDir, dir, "Extensions") if exts, err := os.ReadDir(extDir); err == nil { for _, ext := range exts { if ext.IsDir() && ext.Name() != "Temp" { extCount++ } } } name := names[dir] if name == "" { name = dir } profiles = append(profiles, ChromeProfile{ Dir: dir, Name: name, Extensions: extCount, HasPreferences: true, }) } sort.Slice(profiles, func(i, j int) bool { return profiles[i].Dir < profiles[j].Dir }) return profiles, nil }