diff --git a/lib/common/task.dart b/lib/common/task.dart index 97ba22ae..c7baebec 100644 --- a/lib/common/task.dart +++ b/lib/common/task.dart @@ -102,6 +102,31 @@ Future> makeRealProfileTask( ); } +Map resolveGeoXUrls({ + required Map rawConfig, + required PatchClashConfig patchConfig, +}) { + final profileUrls = Map.from(rawConfig['geox-url'] ?? {}); + final patchUrls = patchConfig.geoXUrl.raw; + final resolvedUrls = {}; + for (final resource in GeoResource.values) { + final key = switch (resource) { + GeoResource.MMDB => 'mmdb', + GeoResource.ASN => 'asn', + GeoResource.GEOIP => 'geoip', + GeoResource.GEOSITE => 'geosite', + }; + final patchUrl = patchUrls[key] ?? defaultGeoXUrl[resource]!; + final profileUrl = profileUrls[key]; + resolvedUrls[key] = patchUrl != defaultGeoXUrl[resource] + ? patchUrl + : profileUrl is String && profileUrl.isNotEmpty + ? profileUrl + : patchUrl; + } + return resolvedUrls; +} + Future> _makeRealProfileTask( MakeRealProfileState data, ) async { @@ -199,7 +224,10 @@ Future> _makeRealProfileTask( } } rawConfig['profile']['store-selected'] = false; - rawConfig['geox-url'] = realPatchConfig.geoXUrl.raw; + rawConfig['geox-url'] = resolveGeoXUrls( + rawConfig: rawConfig, + patchConfig: realPatchConfig, + ); rawConfig['global-ua'] = realPatchConfig.globalUa ?? defaultUA; if (rawConfig['hosts'] == null) { rawConfig['hosts'] = {}; diff --git a/test/common/task_test.dart b/test/common/task_test.dart index 13703b00..45b86cda 100644 --- a/test/common/task_test.dart +++ b/test/common/task_test.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:fl_clash/common/task.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -71,4 +72,74 @@ void main() { expect(profile.a, isNot(contains('recvmsgx: false'))); } }); + + test( + 'profile GEO URLs survive when the app has no custom GEO URL override', + () async { + const profileGeoIpUrl = 'https://profile.example/geoip.dat'; + const profileGeoSiteUrl = 'https://profile.example/geosite.dat'; + final profile = await makeRealProfileTask( + const MakeRealProfileState( + profilesPath: '/tmp/flclash-geo-url-test', + profileId: 12, + rawConfig: { + 'geox-url': { + 'geoip': profileGeoIpUrl, + 'geosite': profileGeoSiteUrl, + }, + }, + realPatchConfig: PatchClashConfig(), + overrideDns: false, + appendSystemDns: false, + proxyGroups: [], + rules: [], + addedRules: [], + defaultUA: 'FlClash-Test', + ), + ); + + expect(profile.a, contains('geoip: "$profileGeoIpUrl"')); + expect(profile.a, contains('geosite: "$profileGeoSiteUrl"')); + }, + ); + + test('custom app GEO URL overrides the profile GEO URL', () { + const appGeoIpUrl = 'https://app.example/geoip.dat'; + final urls = resolveGeoXUrls( + rawConfig: { + 'geox-url': {'geoip': 'https://profile.example/geoip.dat'}, + }, + patchConfig: const PatchClashConfig( + geoXUrl: {GeoResource.GEOIP: appGeoIpUrl}, + ), + ); + + expect(urls['geoip'], appGeoIpUrl); + }); + + test( + 'partial app GEO settings retain defaults for unspecified resources', + () { + const appGeoIpUrl = 'https://app.example/geoip.dat'; + final urls = resolveGeoXUrls( + rawConfig: const {}, + patchConfig: const PatchClashConfig( + geoXUrl: {GeoResource.GEOIP: appGeoIpUrl}, + ), + ); + + expect(urls['geoip'], appGeoIpUrl); + expect(urls['geosite'], defaultGeoXUrl[GeoResource.GEOSITE]); + }, + ); + + test('default GEO URLs are retained when neither source customizes them', () { + final urls = resolveGeoXUrls( + rawConfig: const {}, + patchConfig: const PatchClashConfig(), + ); + + expect(urls['geoip'], defaultGeoXUrl[GeoResource.GEOIP]); + expect(urls['geosite'], defaultGeoXUrl[GeoResource.GEOSITE]); + }); }