Skip to content

Commit 8afb8d4

Browse files
committed
Keep URL extension fallback safe and compatible
1 parent d14802a commit 8afb8d4

5 files changed

Lines changed: 59 additions & 8 deletions

File tree

‎flutter_cache_manager/CHANGELOG.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## [3.4.6] - 2026-10-25
22

3-
* Preserves the URL file extension when a download response omits its content type or uses `application/octet-stream` ([#489](https://github.com/Baseflow/flutter_cache_manager/issues/489)).
3+
* Preserves safe URL file extensions when a download response omits its content type or uses `application/octet-stream` ([#489](https://github.com/Baseflow/flutter_cache_manager/issues/489)).
44

55
## [3.4.5] - 2026-09-18
66

‎flutter_cache_manager/lib/src/compat/file_service_compat.dart‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ class FileServiceCompat extends FileService {
2121

2222
class CompatFileServiceGetResponse implements FileServiceResponse {
2323
final FileFetcherResponse legacyResponse;
24-
final Uri _uri;
24+
final Uri? _uri;
2525
final DateTime _receivedTime = clock.now();
2626

27-
CompatFileServiceGetResponse(this.legacyResponse, this._uri);
27+
CompatFileServiceGetResponse(this.legacyResponse, [this._uri]);
2828

2929
String? _header(String name) {
3030
return legacyResponse.header(name);

‎flutter_cache_manager/lib/src/web/file_service.dart‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ abstract class FileServiceResponse {
6666

6767
/// Basic implementation of a [FileServiceResponse] for http requests.
6868
class HttpGetResponse implements FileServiceResponse {
69-
HttpGetResponse(this._response, this._uri);
69+
HttpGetResponse(this._response, [Uri? uri])
70+
: _uri = uri ?? _response.request?.url;
7071

7172
final DateTime _receivedTime = clock.now();
7273

7374
final http.StreamedResponse _response;
74-
final Uri _uri;
75+
final Uri? _uri;
7576

7677
@override
7778
int get statusCode => _response.statusCode;

‎flutter_cache_manager/lib/src/web/mime_converter.dart‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ extension ContentTypeConverter on ContentType {
55
String get fileExtension => mimeTypes[mimeType] ?? '.$subType';
66
}
77

8-
String fileExtensionFromContentTypeOrUri(String? contentTypeHeader, Uri uri) {
8+
final RegExp _invalidFileExtensionCharacters = RegExp(
9+
r'[\x00-\x1f\x7f/\\:*?"<>|]',
10+
);
11+
12+
String fileExtensionFromContentTypeOrUri(String? contentTypeHeader, Uri? uri) {
913
final contentType = contentTypeHeader == null
1014
? null
1115
: ContentType.parse(contentTypeHeader);
@@ -15,10 +19,17 @@ String fileExtensionFromContentTypeOrUri(String? contentTypeHeader, Uri uri) {
1519
return contentType.fileExtension;
1620
}
1721

18-
final fileName = uri.pathSegments.isEmpty ? '' : uri.pathSegments.last;
22+
final pathSegments = uri?.pathSegments;
23+
final fileName = pathSegments == null || pathSegments.isEmpty
24+
? ''
25+
: pathSegments.last;
1926
final extensionIndex = fileName.lastIndexOf('.');
2027
if (extensionIndex > 0 && extensionIndex < fileName.length - 1) {
21-
return fileName.substring(extensionIndex);
28+
final extension = fileName.substring(extensionIndex);
29+
if (!_invalidFileExtensionCharacters.hasMatch(extension) &&
30+
!extension.endsWith(' ')) {
31+
return extension;
32+
}
2233
}
2334

2435
return contentType?.fileExtension ?? '';

‎flutter_cache_manager/test/http_file_fetcher_test.dart‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,19 @@ void main() {
8080
expect(response.fileExtension, '.$fileExtension');
8181
});
8282

83+
test('HttpGetResponse keeps its original one-argument constructor', () {
84+
final response = HttpGetResponse(
85+
StreamedResponse(
86+
const Stream<List<int>>.empty(),
87+
200,
88+
headers: {'content-type': 'application/octet-stream'},
89+
request: Request('GET', Uri.parse('https://example.com/video.mp4')),
90+
),
91+
);
92+
93+
expect(response.fileExtension, '.mp4');
94+
});
95+
8396
test('Download uses the URL extension for generic content type', () async {
8497
const url = 'https://example.com/video.mp4?token=secret';
8598
final client = MockClient((_) async {
@@ -177,6 +190,32 @@ void main() {
177190
expect(response.fileExtension, '.bin');
178191
});
179192

193+
test(
194+
'Encoded path separators are not used in cached file extensions',
195+
() async {
196+
const url = 'https://example.com/video.mp4%2Fpart';
197+
final client = MockClient((_) async {
198+
return Response.bytes(
199+
Uint8List(16),
200+
200,
201+
headers: {'content-type': 'application/octet-stream'},
202+
);
203+
});
204+
final cacheManager = CacheManager(
205+
Config(
206+
'test',
207+
fileSystem: TestFileSystem(),
208+
repo: MockCacheInfoRepository(),
209+
fileService: HttpFileService(httpClient: client),
210+
),
211+
);
212+
213+
final result = await cacheManager.downloadFile(url);
214+
215+
expect(result.file.path, endsWith('.bin'));
216+
},
217+
);
218+
180219
test('Test CompatFileService', () async {
181220
var eTag = 'test';
182221
var fileExtension = 'jpg';

0 commit comments

Comments
 (0)