feat: add proxy labels and default profiles

This commit is contained in:
QiuSW
2026-07-27 12:09:54 +08:00
parent a8b7277dab
commit 0f25f39480
10 changed files with 397 additions and 115 deletions
+71 -8
View File
@@ -68,6 +68,27 @@ func DefaultPath() (string, error) {
return defaultPathForExecutable(executable)
}
// DefaultInstanceUserDataRoot returns the portable root for new browser
// profiles. It resolves the actual executable location and never uses cwd.
func DefaultInstanceUserDataRoot() (string, error) {
executable, err := os.Executable()
if err != nil {
return "", fmt.Errorf("%w: resolve executable", ErrDefaultConfigUnavailable)
}
return defaultInstanceUserDataRootForExecutable(executable)
}
// DefaultInstanceUserDataDir returns a unique-instance profile path below the
// executable-side user_data_dirs root. It only computes a path; it does not
// create directories or validate their write permissions.
func DefaultInstanceUserDataDir(instanceID string) (string, error) {
root, err := DefaultInstanceUserDataRoot()
if err != nil {
return "", err
}
return defaultInstanceUserDataDir(root, instanceID)
}
// LegacyPath is the pre-portable configuration location. It is read only when
// the executable-side config does not exist yet.
func LegacyPath() (string, error) {
@@ -107,6 +128,23 @@ func defaultPathForExecutable(executable string) (string, error) {
return filepath.Join(filepath.Dir(filepath.Clean(path)), "config.json"), nil
}
func defaultInstanceUserDataRootForExecutable(executable string) (string, error) {
path := strings.TrimSpace(executable)
if path == "" || !filepath.IsAbs(path) {
return "", fmt.Errorf("%w: executable path must be absolute", ErrDefaultConfigUnavailable)
}
return filepath.Join(filepath.Dir(filepath.Clean(path)), "user_data_dirs"), nil
}
func defaultInstanceUserDataDir(root, instanceID string) (string, error) {
root = strings.TrimSpace(root)
instanceID = strings.TrimSpace(instanceID)
if root == "" || !filepath.IsAbs(root) || instanceID == "" || filepath.Base(instanceID) != instanceID || instanceID == "." {
return "", fmt.Errorf("%w: invalid default instance directory", ErrDefaultConfigUnavailable)
}
return filepath.Join(filepath.Clean(root), instanceID), nil
}
func openDefaultAt(target, legacy string) (*Store, error) {
store, targetExists, err := defaultStore(target)
if err != nil || targetExists {
@@ -303,28 +341,35 @@ func normalizeProxyConfig(value *File) error {
return errors.New("config is required")
}
byID := make(map[string]ProxyProfile, len(value.Proxies))
names := make(map[string]struct{}, len(value.Proxies))
names := make([]string, 0, len(value.Proxies))
servers := make(map[string]struct{}, len(value.Proxies))
for i := range value.Proxies {
profile := &value.Proxies[i]
profile.ID = strings.TrimSpace(profile.ID)
profile.Name = strings.TrimSpace(profile.Name)
if profile.ID == "" || profile.Name == "" {
name, err := domain.NormalizeProxyName(profile.Name)
if profile.ID == "" || err != nil {
return errors.New("invalid proxy configuration")
}
profile.Name = name
if _, exists := byID[profile.ID]; exists {
return errors.New("duplicate proxy configuration")
}
nameKey := strings.ToLower(profile.Name)
if _, exists := names[nameKey]; exists {
return errors.New("duplicate proxy configuration")
for _, existing := range names {
if strings.EqualFold(existing, profile.Name) {
return errors.New("duplicate proxy configuration")
}
}
server, err := domain.NormalizeProxyServer(profile.Server)
if err != nil || server == "" {
return errors.New("invalid proxy configuration")
}
if _, exists := servers[server]; exists {
return errors.New("duplicate proxy configuration")
}
profile.Server = server
byID[profile.ID] = *profile
names[nameKey] = struct{}{}
names = append(names, profile.Name)
servers[server] = struct{}{}
}
legacyByServer := make(map[string]string, len(value.Proxies))
@@ -351,10 +396,12 @@ func normalizeProxyConfig(value *File) error {
proxyID := legacyByServer[server]
if proxyID == "" {
proxyID = nextLegacyProxyID(byID)
profile := ProxyProfile{ID: proxyID, Name: "导入代理 " + strconv.Itoa(len(value.Proxies)+1), Server: server}
profile := ProxyProfile{ID: proxyID, Name: nextLegacyProxyName(names), Server: server}
value.Proxies = append(value.Proxies, profile)
byID[proxyID] = profile
legacyByServer[server] = proxyID
names = append(names, profile.Name)
servers[server] = struct{}{}
}
instance.ProxyID = proxyID
instance.Launch.ProxyServer = ""
@@ -371,6 +418,22 @@ func nextLegacyProxyID(existing map[string]ProxyProfile) string {
}
}
func nextLegacyProxyName(existing []string) string {
for index := 1; ; index++ {
candidate := "导入代理 " + strconv.Itoa(index)
duplicate := false
for _, name := range existing {
if strings.EqualFold(name, candidate) {
duplicate = true
break
}
}
if !duplicate {
return candidate
}
}
}
func replace(target, temp string) error {
backup := target + ".bak"
_, statErr := os.Stat(target)
+39
View File
@@ -112,6 +112,28 @@ func TestDefaultPathForExecutableUsesExecutableDirectory(t *testing.T) {
}
}
func TestDefaultInstanceUserDataDirectoryUsesExecutableDirectoryAndStableID(t *testing.T) {
executable := filepath.Join(t.TempDir(), "published", "chub.exe")
root, err := defaultInstanceUserDataRootForExecutable(executable)
if err != nil || root != filepath.Join(filepath.Dir(executable), "user_data_dirs") {
t.Fatalf("defaultInstanceUserDataRootForExecutable() = %q, %v", root, err)
}
first, err := defaultInstanceUserDataDir(root, "instance-one")
if err != nil || first != filepath.Join(root, "instance-one") {
t.Fatalf("defaultInstanceUserDataDir() = %q, %v", first, err)
}
second, err := defaultInstanceUserDataDir(root, "instance-two")
if err != nil || second == first {
t.Fatalf("distinct instance directory = %q, %v", second, err)
}
if _, err := defaultInstanceUserDataRootForExecutable("chub.exe"); !errors.Is(err, ErrDefaultConfigUnavailable) {
t.Fatalf("relative executable root error = %v", err)
}
if _, err := defaultInstanceUserDataDir(root, `..\other`); !errors.Is(err, ErrDefaultConfigUnavailable) {
t.Fatalf("unsafe instance id error = %v", err)
}
}
func TestOpenDefaultAtMigratesValidLegacyWithoutRemovingIt(t *testing.T) {
directory := t.TempDir()
target := filepath.Join(directory, "portable", "config.json")
@@ -249,6 +271,23 @@ func TestStoreRoundTripsReferencedProxyWithoutCredentials(t *testing.T) {
}
}
func TestStoreRejectsBlankOrDuplicateProxyNamesAndServers(t *testing.T) {
store, err := New(filepath.Join(t.TempDir(), "config.json"))
if err != nil {
t.Fatal(err)
}
for _, proxies := range [][]ProxyProfile{
{{ID: "blank", Name: " ", Server: "http://127.0.0.1:8080"}},
{{ID: "one", Name: "新加坡", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "新加坡", Server: "http://127.0.0.1:8081"}},
{{ID: "one", Name: "Singapore", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "sINGAPORE", Server: "http://127.0.0.1:8081"}},
{{ID: "one", Name: "新加坡", Server: "http://127.0.0.1:8080"}, {ID: "two", Name: "东京", Server: "HTTP://127.0.0.1:8080"}},
} {
if err := store.Save(File{Proxies: proxies}); err == nil {
t.Fatalf("unsafe proxy profiles persisted: %#v", proxies)
}
}
}
func TestStoreMigratesLegacyProxyAndRejectsUnsafeConfig(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.json")
if err := os.WriteFile(path, []byte(`{"version":1,"settings":{},"instances":[{"id":"a","name":"运营","launch":{"Kind":"chrome","UserDataDir":"C:\\profiles\\a","ProxyServer":"http://127.0.0.1:8080"}}]}`), 0o600); err != nil {