Files
HackBrowserData/browser/chromium/extract_extension.go
T
Roger b3bbc0dadf feat: add CountEntries to skip decryption for list --detail (#562)
* feat: add CountEntries to skip decryption for list --detail (#549)
* test: add CountEntries and countCategory tests at browser level
* fix: address review feedback on CountRows and countLocalStorage
* test: add CountRows unit tests
2026-04-07 22:28:39 +08:00

130 lines
3.5 KiB
Go

package chromium
import (
"fmt"
"os"
"github.com/tidwall/gjson"
"github.com/moond4rk/hackbrowserdata/types"
)
// defaultExtensionKeys are the JSON paths tried for standard Chromium browsers.
var defaultExtensionKeys = []string{
"extensions.settings",
"settings.extensions",
"settings.settings",
}
func extractExtensions(path string) ([]types.ExtensionEntry, error) {
return extractExtensionsWithKeys(path, defaultExtensionKeys)
}
// extractExtensionsWithKeys reads Secure Preferences and looks for extension
// settings under the given JSON key paths. This allows browser variants
// (e.g. Opera with "extensions.opsettings") to reuse the same parsing logic.
func extractExtensionsWithKeys(path string, keys []string) ([]types.ExtensionEntry, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
var settings gjson.Result
for _, key := range keys {
settings = gjson.GetBytes(data, key)
if settings.Exists() {
break
}
}
if !settings.Exists() {
return nil, fmt.Errorf("cannot find extensions in settings")
}
var extensions []types.ExtensionEntry
settings.ForEach(func(id, ext gjson.Result) bool {
// Skip system/component extensions
// https://source.chromium.org/chromium/chromium/src/+/main:extensions/common/mojom/manifest.mojom
location := ext.Get("location").Int()
if location == 5 || location == 10 {
return true
}
manifest := ext.Get("manifest")
if !manifest.Exists() {
return true
}
extensions = append(extensions, types.ExtensionEntry{
Name: manifest.Get("name").String(),
ID: id.String(),
Description: manifest.Get("description").String(),
Version: manifest.Get("version").String(),
HomepageURL: manifest.Get("homepage_url").String(),
Enabled: isExtensionEnabled(ext),
})
return true
})
return extensions, nil
}
// isExtensionEnabled checks whether an extension is enabled.
// Modern Chrome uses disable_reasons (array): empty [] = enabled, non-empty [1] = disabled.
// Older Chrome uses state (int): 1 = enabled.
func isExtensionEnabled(ext gjson.Result) bool {
reasons := ext.Get("disable_reasons")
if reasons.Exists() {
return reasons.IsArray() && len(reasons.Array()) == 0
}
return ext.Get("state").Int() == 1
}
// extractOperaExtensions extracts extensions from Opera's Secure Preferences,
// which stores extension data under "extensions.opsettings" instead of the
// standard "extensions.settings".
func extractOperaExtensions(path string) ([]types.ExtensionEntry, error) {
return extractExtensionsWithKeys(path, []string{"extensions.opsettings"})
}
func countExtensions(path string) (int, error) {
return countExtensionsWithKeys(path, defaultExtensionKeys)
}
func countOperaExtensions(path string) (int, error) {
return countExtensionsWithKeys(path, []string{"extensions.opsettings"})
}
// countExtensionsWithKeys counts non-system extensions without building
// full ExtensionEntry structs. Mirrors the filtering logic in extractExtensionsWithKeys.
func countExtensionsWithKeys(path string, keys []string) (int, error) {
data, err := os.ReadFile(path)
if err != nil {
return 0, err
}
var settings gjson.Result
for _, key := range keys {
settings = gjson.GetBytes(data, key)
if settings.Exists() {
break
}
}
if !settings.Exists() {
return 0, nil
}
var count int
settings.ForEach(func(_, ext gjson.Result) bool {
location := ext.Get("location").Int()
if location == 5 || location == 10 {
return true
}
if !ext.Get("manifest").Exists() {
return true
}
count++
return true
})
return count, nil
}