Skip to content

Commit 51b65d7

Browse files
committed
Merge branch 'feat/add-linked-files' into develop
2 parents 74e6432 + ed4d755 commit 51b65d7

File tree

1 file changed

+58
-13
lines changed

1 file changed

+58
-13
lines changed

lib/src/asset_loader.dart

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ abstract class AssetLoader {
2424
/// default used is RootBundleAssetLoader which uses flutter's assetloader
2525
///
2626
class RootBundleAssetLoader extends AssetLoader {
27+
// Place inside class RootBundleAssetLoader
28+
static const int _maxLinkedDepth = 32;
29+
2730
const RootBundleAssetLoader();
2831

2932
String getLocalePath(String basePath, Locale locale) {
@@ -35,28 +38,64 @@ class RootBundleAssetLoader extends AssetLoader {
3538
}
3639

3740
Future<Map<String, dynamic>> _getLinkedTranslationFileDataFromBaseJson(
38-
String basePath, Locale locale, Map<String, dynamic> baseJson,
39-
{List<String> fileLoaded = const []}) async {
40-
Map<String, dynamic> fullJson = Map<String, dynamic>.from(baseJson);
41+
String basePath,
42+
Locale locale,
43+
Map<String, dynamic> baseJson, {
44+
required Set<String> visited,
45+
required Map<String, Map<String, dynamic>> cache,
46+
int depth = 0,
47+
}) async {
48+
if (depth > _maxLinkedDepth) {
49+
throw StateError('Maximum linked files depth ($_maxLinkedDepth) exceeded for $locale at $basePath.');
50+
}
51+
52+
final Map<String, dynamic> fullJson = Map<String, dynamic>.from(baseJson);
4153

42-
for (var entry in baseJson.entries) {
43-
var key = entry.key;
54+
for (final entry in baseJson.entries) {
55+
final key = entry.key;
4456
var value = entry.value;
4557

4658
if (value is String && value.startsWith(':/')) {
47-
String filePath = value.substring(2);
59+
final rawPath = value.substring(2).trim();
60+
// Normalize and reject traversal
61+
final normalizedPath = rawPath.replaceAll(RegExp(r'^[\\/]+'), '');
62+
if (normalizedPath.contains('..')) {
63+
throw FormatException('Invalid linked file path "$rawPath" for key "$key".');
64+
}
65+
final linkedAssetPath = _getLinkedLocalePath(basePath, normalizedPath, locale);
4866

49-
if (fileLoaded.contains(filePath)) {
50-
throw Exception('Circular reference detected: $filePath is loaded multiple times');
67+
if (visited.contains(linkedAssetPath)) {
68+
throw StateError('Cyclic linked files detected at "$linkedAssetPath" (key: "$key").');
5169
}
5270

53-
fileLoaded.add(filePath);
54-
value = json.decode(await rootBundle.loadString(_getLinkedLocalePath(basePath, filePath, locale)));
71+
final Map<String, dynamic> linkedJson = cache[linkedAssetPath] ??
72+
(cache[linkedAssetPath] =
73+
(json.decode(await rootBundle.loadString(linkedAssetPath)) as Map<String, dynamic>));
74+
75+
visited.add(linkedAssetPath);
76+
try {
77+
value = await _getLinkedTranslationFileDataFromBaseJson(
78+
basePath,
79+
locale,
80+
linkedJson,
81+
visited: visited,
82+
cache: cache,
83+
depth: depth + 1,
84+
);
85+
} finally {
86+
visited.remove(linkedAssetPath);
87+
}
5588
}
5689

5790
if (value is Map<String, dynamic>) {
58-
fullJson[key] =
59-
await _getLinkedTranslationFileDataFromBaseJson(basePath, locale, value, fileLoaded: fileLoaded);
91+
fullJson[key] = await _getLinkedTranslationFileDataFromBaseJson(
92+
basePath,
93+
locale,
94+
value,
95+
visited: visited,
96+
cache: cache,
97+
depth: depth + 1,
98+
);
6099
}
61100
}
62101

@@ -69,6 +108,12 @@ class RootBundleAssetLoader extends AssetLoader {
69108
EasyLocalization.logger.debug('Load asset from $path');
70109

71110
Map<String, dynamic> baseJson = json.decode(await rootBundle.loadString(localePath));
72-
return await _getLinkedTranslationFileDataFromBaseJson(path, locale, baseJson);
111+
return await _getLinkedTranslationFileDataFromBaseJson(
112+
path,
113+
locale,
114+
baseJson,
115+
visited: <String>{},
116+
cache: <String, Map<String, dynamic>>{},
117+
);
73118
}
74119
}

0 commit comments

Comments
 (0)