diff --git a/mobile/lib/shared/animated_avatar.dart b/mobile/lib/shared/animated_avatar.dart index 5393628e60..fad174301f 100644 --- a/mobile/lib/shared/animated_avatar.dart +++ b/mobile/lib/shared/animated_avatar.dart @@ -31,7 +31,7 @@ String buildAnimatedAvatarUrl(String posterUrl, String animationUrl) => /// Returns `null` when the poster or animation URL is missing, malformed, or /// does not use HTTP(S). AnimatedAvatarDescriptor? parseAnimatedAvatarUrl(String? url) { - if (url == null || url.isEmpty) return null; + if (url == null || !_startsWithHttp(url)) return null; final separatorIndex = url.indexOf(_animatedAvatarSeparator); if (separatorIndex <= 0) return null; @@ -58,6 +58,15 @@ AnimatedAvatarDescriptor? parseAnimatedAvatarUrl(String? url) { ); } +/// Whether [url] has an HTTP(S) scheme, checked without reading the rest of +/// it. Avatars are checked on every build, and a `data:` profile picture of +/// 100 KB costs about a millisecond to scan and parse. The authority is left to +/// `Uri`, which also accepts backslashes there. +bool _startsWithHttp(String url) { + final head = url.substring(0, url.length < 6 ? url.length : 6).toLowerCase(); + return head.startsWith('http:') || head.startsWith('https:'); +} + bool _isHttpUrl(String value) { final uri = Uri.tryParse(value); return uri != null && @@ -68,8 +77,9 @@ bool _isHttpUrl(String value) { /// Character presets can be saved as plain poster URLs without buzz-anim. /// Keep photographs and ordinary emoji avatars at their existing size. bool usesInsetAgentArtwork(String? url) { + if (url == null || !_startsWithHttp(url)) return false; if (parseAnimatedAvatarUrl(url) != null) return true; - final uri = url == null ? null : Uri.tryParse(url); + final uri = Uri.tryParse(url); return uri != null && uri.scheme == 'https' && uri.host == 'dwwgwmfqqjotj.cloudfront.net' && diff --git a/mobile/test/shared/animated_avatar_test.dart b/mobile/test/shared/animated_avatar_test.dart index e3b66ec601..3f5a122aa1 100644 --- a/mobile/test/shared/animated_avatar_test.dart +++ b/mobile/test/shared/animated_avatar_test.dart @@ -40,4 +40,25 @@ void main() { isNull, ); }); + + test('only HTTP(S) URLs, in any case, can be preset artwork', () { + const preset = 'dwwgwmfqqjotj.cloudfront.net/avatars/a/poster/b/c.png'; + expect(usesInsetAgentArtwork('https://$preset'), isTrue); + expect(usesInsetAgentArtwork('HTTPS://$preset'), isTrue); + expect(usesInsetAgentArtwork(r'https:\\' + preset), isTrue); + expect(usesInsetAgentArtwork(r'https:/\' + preset), isTrue); + expect(usesInsetAgentArtwork(r'https:\/' + preset), isTrue); + expect(usesInsetAgentArtwork('http://$preset'), isFalse); + expect(usesInsetAgentArtwork(' https://$preset'), isFalse); + expect( + usesInsetAgentArtwork('data:image/png;base64,${'A' * 1000}'), + isFalse, + ); + expect(usesInsetAgentArtwork(''), isFalse); + expect(usesInsetAgentArtwork(null), isFalse); + expect( + usesInsetAgentArtwork(buildAnimatedAvatarUrl(posterUrl, animationUrl)), + isTrue, + ); + }); }