CVE-2026-79223
Overview
Background
- Skottie
- Skia’s Lottie animation renderer that parses attacker-controllable JSON animation files, including shape-layer gradient definitions.
- Gradient stop array (`fStops`)
- a flat buffer that encodes gradient stops as
ColorRecentries (4 elements each) followed byOpacityRecentries (2 elements each), whose partition must be recomputed fromfStopCount. - `SkSafeMath`
- a Skia helper that performs
size_tarithmetic while latching anfOKflag whenever an operation overflows or underflows. - 32-bit build
- a build where
size_tis 32 bits, so multiplications by 4 and subtractions wrap silently at 2^32.
Root Cause Analysis
In Sync::onSync (the enclosing function is not named in the hunk, so this is (inferred)) the code partitions fStops into color stops and opacity stops by computing c_size = c_count * 4 and o_count = (fStops.size() - c_size) / 2 using ordinary size_t arithmetic, where c_count = fStopCount is derived from the animation JSON. On a 32-bit build a large fStopCount makes c_count * 4 wrap around modulo 2^32, and fStops.size() - c_size can underflow, so c_size becomes small while the derived counts are nonsensical. The pre-patch consistency check fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2) re-uses the same overflowing expressions, so a crafted fStopCount can make the recomputed total equal fStops.size() and pass validation despite the counts being wrong.
The fix routes every step (mul, sub, add) through an SkSafeMath safe instance and rejects the gradient when !safe.ok(), so any overflow or underflow now fails the check instead of silently producing an in-range but incorrect partition that downstream gradient construction would consume with mismatched color/opacity counts.
size_t arithmetic it was meant to detect, so an overflowing fStopCount could satisfy the equality check; the fix performs the arithmetic with SkSafeMath and bails on any overflow.Attack Path
- Deliver malicious animation An attacker supplies a Lottie/Skottie JSON animation (e.g., via a web page or app that renders Skottie content) containing a shape-layer gradient with a crafted stop count.
- Overflow the count math
On a 32-bit build, the large
fStopCountcausesc_count * 4(and thefStops.size() - c_sizesubtraction) to wrap, producing a bogus but smallc_sizeand inconsistento_count. - Bypass the consistency check
Because the pre-patch guard re-evaluates the same overflowing expressions, the attacker tunes
fStopCountsofStops.size() == c_count * 4 + o_count * 2holds under wraparound, passing validation. - Consume a mismatched partition
Gradient construction then treats
fStopswith an incorrect color/opacity split, reading past or misinterpreting the buffer (specific out-of-bounds sink not shown in the diff)(inferred).
Impact Assessment
(inferred). This occurs in whichever process renders the animation (renderer or app process using Skia). The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8.Changed Functions
| Function | Change | Notes |
|---|---|---|
DEF_TESTmodules/skottie/src/SkottieTest.cpp |
modified | |
TestMarkerObservermodules/skottie/src/SkottieTest.cpp |
modified | |
TestResourceProvidermodules/skottie/src/SkottieTest.cpp |
modified | |
TestAssetmodules/skottie/src/SkottieTest.cpp |
modified | |
DEF_TESTmodules/skottie/tests/SkottieTest.cpp |
modified |
Files Changed
modules/skottie/tests/SkottieTest.cppmodules/skottie/BUILD.gnsrc/core/SkSafeMath.hmodules/skottie/src/layers/shapelayer/Gradient.cpp
Audit Directions
- Overflow-checked-with-itself validationLook for size/count consistency checks that recompute quantities using the same unchecked arithmetic they are supposed to validate, especially
count * elem_sizepatterns. - 32-bit `size_t` arithmetic on parsed countsAudit parsers that multiply or subtract attacker-supplied element counts against buffer sizes without
SkSafeMath/base::CheckedNumeric, since these wrap silently on 32-bit builds. - Flat heterogeneous buffersReview code that partitions a single flat array into differently sized record types based on a separately stored count, where a mismatched split leads to misread records.
Patch
From 36e509ddad66569e1a604816c1e47845950eee77 Mon Sep 17 00:00:00 2001
From: Florin Malita <fmalita@google.com>
Date: Mon, 13 Jul 2026 12:15:44 -0400
Subject: [PATCH] [skottie] Fix gradient count overflow in 32 bit builds
Use SkSafeMath to detect size_t overflows.
Also relocate SkottieTest.cpp from src/ to tests/.
Bug: b/530816571
Change-Id: I861fe30b5223e2fb074c37217afa7d3bd48419ca
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1292156
Commit-Queue: Florin Malita <fmalita@google.com>
Reviewed-by: Brandon Wilson <bwils@google.com>
---
diff --git a/modules/skottie/src/SkottieTest.cpp b/modules/skottie/src/SkottieTest.cpp
deleted file mode 100644
index 76bd5c3..0000000
--- a/modules/skottie/src/SkottieTest.cpp
+++ /dev/null
@@ -1,271 +0,0 @@
-/*
- * Copyright 2018 Google LLC
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "include/core/SkStream.h"
-#include "include/core/SkSurface.h"
-#include "modules/skottie/include/Skottie.h"
-#include "tests/Test.h"
-
-#include <cmath>
-#include <string>
-#include <tuple>
-#include <vector>
-
-using namespace skottie;
-
-DEF_TEST(Skottie_OssFuzz8956, reporter) {
- static constexpr char json[] =
- "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
- " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
- " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
-
- SkMemoryStream stream(json, strlen(json));
-
- // Passes if parsing doesn't crash.
- auto animation = Animation::Make(&stream);
-}
-
-DEF_TEST(Skottie_Annotations, reporter) {
- static constexpr char json[] = R"({
- "v": "5.2.1",
- "w": 100,
- "h": 100,
- "fr": 10,
- "ip": 0,
- "op": 100,
- "layers": [
- {
- "ty": 1,
- "ind": 0,
- "ip": 0,
- "op": 1,
- "ks": {
- "o": { "a": 0, "k": 50 }
- },
- "sw": 100,
- "sh": 100,
- "sc": "#ffffff"
- }
- ],
- "markers": [
- {
- "cm": "marker_1",
- "dr": 25,
- "tm": 25
- },
- {
- "cm": "marker_2",
- "dr": 0,
- "tm": 75
- }
- ]
- })";
-
- class TestMarkerObserver final : public MarkerObserver {
- public:
- void onMarker(const char name[], float t0, float t1) override {
- fMarkers.push_back(std::make_tuple(name, t0, t1));
- }
-
- std::vector<std::tuple<std::string, float, float>> fMarkers;
- };
-
- SkMemoryStream stream(json, strlen(json));
- auto observer = sk_make_sp<TestMarkerObserver>();
-
- auto animation = skottie::Animation::Builder()
- .setMarkerObserver(observer)
- .make(&stream);
-
- REPORTER_ASSERT(reporter, animation);
- REPORTER_ASSERT(reporter, animation->duration() == 10);
- REPORTER_ASSERT(reporter, animation->inPoint() == 0.0);
- REPORTER_ASSERT(reporter, animation->outPoint() == 100.0);
-
- REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
- REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
- REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
- REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
- REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
- REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
- REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
-}
-
-DEF_TEST(Skottie_Image_Loading, reporter) {
- class TestResourceProvider final : public skresources::ResourceProvider {
- public:
- TestResourceProvider(sk_sp<skresources::ImageAsset> single_asset,
- sk_sp<skresources::ImageAsset> multi_asset)
- : fSingleFrameAsset(std::move(single_asset))
- , fMultiFrameAsset (std::move( multi_asset)) {}
-
- private:
- sk_sp<ImageAsset> loadImageAsset(const char path[],
- const char name[],
- const char id[]) const override {
- return strcmp(id, "single_frame")
- ? fMultiFrameAsset
- : fSingleFrameAsset;
- }
-
- const sk_sp<skresources::ImageAsset> fSingleFrameAsset,
- fMultiFrameAsset;
- };
-
- auto make_animation = [&reporter] (sk_sp<skresources::ImageAsset> single_asset,
- sk_sp<skresources::ImageAsset> multi_asset,
- bool deferred_image_loading) {
- static constexpr char json[] = R"({
- "v": "5.2.1",
- "w": 100,
- "h": 100,
- "fr": 10,
- "ip": 0,
- "op": 100,
- "assets": [
- {
- "id": "single_frame",
- "p" : "single_frame.png",
- "u" : "images/",
- "w" : 500,
- "h" : 500
- },
- {
- "id": "multi_frame",
- "p" : "multi_frame.png",
- "u" : "images/",
- "w" : 500,
- "h" : 500
- }
- ],
- "layers": [
- {
- "ty": 2,
- "refId": "single_frame",
- "ind": 0,
- "ip": 0,
- "op": 100,
- "ks": {}
- },
- {
- "ty": 2,
- "refId": "multi_frame",
- "ind": 1,
- "ip": 0,
- "op": 100,
- "ks": {}
- }
- ]
- })";
-
- SkMemoryStream stream(json, strlen(json));
-
- const auto flags = deferred_image_loading
- ? static_cast<uint32_t>(skottie::Animation::Builder::kDeferImageLoading)
- : 0;
- auto animation =
- skottie::Animation::Builder(flags)
- .setResourceProvider(sk_make_sp<TestResourceProvider>(std::move(single_asset),
- std::move( multi_asset)))
- .make(&stream);
-
- REPORTER_ASSERT(reporter, animation);
-
- return animation;
- };
-
- class TestAsset final : public skresources::ImageAsset {
- public:
- explicit TestAsset(bool multi_frame) : fMultiFrame(multi_frame) {}
-
- const std::vector<float>& requestedFrames() const { return fRequestedFrames; }
-
- private:
- bool isMultiFrame() override { return fMultiFrame; }
-
- sk_sp<SkImage> getFrame(float t) override {
- fRequestedFrames.push_back(t);
-
- return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))->makeImageSnapshot();
- }
-
- const bool fMultiFrame;
-
- std::vector<float> fRequestedFrames;
- };
-
- {
- auto single_asset = sk_make_sp<TestAsset>(false),
- multi_asset = sk_make_sp<TestAsset>(true);
-
- // Default image loading: single-frame images are loaded upfront, multi-frame images are
- // loaded on-demand.
- auto animation = make_animation(single_asset, multi_asset, false);
-
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 0);
- REPORTER_ASSERT(reporter, SkScalarNearlyZero(single_asset->requestedFrames()[0]));
-
- animation->seekFrameTime(1);
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[0], 1));
-
- animation->seekFrameTime(2);
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 2);
- REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
- }
-
- {
- auto single_asset = sk_make_sp<TestAsset>(false),
- multi_asset = sk_make_sp<TestAsset>(true);
-
- // Deferred image loading: both single-frame and multi-frame images are loaded on-demand.
- auto animation = make_animation(single_asset, multi_asset, true);
-
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 0);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 0);
-
- animation->seekFrameTime(1);
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, SkScalarNearlyEqual(single_asset->requestedFrames()[0], 1));
- REPORTER_ASSERT(reporter, SkScalarNearlyEqual (multi_asset->requestedFrames()[0], 1));
-
- animation->seekFrameTime(2);
- REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
- REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 2);
- REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
- }
-}
-
-DEF_TEST(Skottie_Layer_NoType, r) {
- static constexpr char json[] =
- R"({
- "v": "5.2.1",
- "w": 100,
- "h": 100,
- "fr": 10,
- "ip": 0,
- "op": 100,
- "layers": [
- {
- "ind": 0,
- "ip": 0,
- "op": 100,
- "ks": {}
- }
- ]
- })";
-
- SkMemoryStream stream(json, strlen(json));
- auto anim = Animation::Make(&stream);
-
- // passes if we don't crash
- REPORTER_ASSERT(r, anim);
-}
diff --git a/modules/skottie/src/layers/shapelayer/Gradient.cpp b/modules/skottie/src/layers/shapelayer/Gradient.cpp
index 418ba63..069fb15 100644
--- a/modules/skottie/src/layers/shapelayer/Gradient.cpp
+++ b/modules/skottie/src/layers/shapelayer/Gradient.cpp
@@ -23,6 +23,7 @@
#include "modules/sksg/include/SkSGGradient.h"
#include "modules/sksg/include/SkSGPaint.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
+#include "src/core/SkSafeMath.h"
#include <algorithm>
#include <cstddef>
@@ -141,10 +142,13 @@
// while the number of opacity stops is implicit (based on the size of fStops).
//
// |fStops| holds ColorRec x |fColorStopCount| + OpacityRec x N
- const auto c_count = fStopCount,
- c_size = c_count * 4,
- o_count = (fStops.size() - c_size) / 2;
- if (fStops.size() < c_size || fStops.size() != (c_count * 4 + o_count * 2)) {
+ SkSafeMath safe;
+ const size_t c_count = fStopCount,
+ c_size = safe.mul(c_count, 4),
+ o_count = safe.sub(fStops.size(), c_size) / 2,
+ o_size = safe.mul(o_count, 2),
+ total_size = safe.add(c_size, o_size);
+ if (!safe.ok() || fStops.size() != total_size) {
// apply() may get called before the stops are set, so only log when we have some stops.
if (!fStops.empty()) {
SkDebugf("!! Invalid gradient stop array size: %zu\n", fStops.size());
diff --git a/src/core/SkSafeMath.h b/src/core/SkSafeMath.h
index 31a4144..74c5a5c 100644
--- a/src/core/SkSafeMath.h
+++ b/src/core/SkSafeMath.h
@@ -36,6 +36,11 @@
return result;
}
+ size_t sub(size_t x, size_t y) {
+ fOK &= x >= y;
+ return x - y;
+ }
+
int addInt(int a, int b) {
int64_t result = static_cast<int64_t>(a) + static_cast<int64_t>(b);
if (!SkTFitsIn<int>(result)) {
diff --git a/modules/skottie/tests/SkottieTest.cpp b/modules/skottie/tests/SkottieTest.cpp
new file mode 100644
index 0000000..472c2bd
--- /dev/null
+++ b/modules/skottie/tests/SkottieTest.cpp
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkStream.h"
+#include "include/core/SkSurface.h"
+#include "modules/skottie/include/Skottie.h"
+#include "tests/Test.h"
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+using namespace skottie;
+
+DEF_TEST(Skottie_OssFuzz8956, reporter) {
+ static constexpr char json[] =
+ "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
+ " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
+ " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
+
+ SkMemoryStream stream(json, strlen(json));
+
+ // Passes if parsing doesn't crash.
+ auto animation = Animation::Make(&stream);
+}
+
+DEF_TEST(Skottie_Annotations, reporter) {
+ static constexpr char json[] = R"({
+ "v": "5.2.1",
+ "w": 100,
+ "h": 100,
+ "fr": 10,
+ "ip": 0,
+ "op": 100,
+ "layers": [
+ {
+ "ty": 1,
+ "ind": 0,
+ "ip": 0,
+ "op": 1,
+ "ks": {
+ "o": { "a": 0, "k": 50 }
+ },
+ "sw": 100,
+ "sh": 100,
+ "sc": "#ffffff"
+ }
+ ],
+ "markers": [
+ {
+ "cm": "marker_1",
+ "dr": 25,
... (file diff truncated)
Regression Test / PoC
diff --git a/modules/skottie/tests/SkottieTest.cpp b/modules/skottie/tests/SkottieTest.cpp
new file mode 100644
index 0000000..472c2bd
--- /dev/null
+++ b/modules/skottie/tests/SkottieTest.cpp
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2018 Google LLC
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "include/core/SkStream.h"
+#include "include/core/SkSurface.h"
+#include "modules/skottie/include/Skottie.h"
+#include "tests/Test.h"
+
+#include <string>
+#include <tuple>
+#include <vector>
+
+using namespace skottie;
+
+DEF_TEST(Skottie_OssFuzz8956, reporter) {
+ static constexpr char json[] =
+ "{\"v\":\" \",\"fr\":3,\"w\":4,\"h\":3,\"layers\":[{\"ty\": 1, \"sw\": 10, \"sh\": 10,"
+ " \"sc\":\"#ffffff\", \"ks\":{\"o\":{\"a\": true, \"k\":"
+ " [{\"t\": 0, \"s\": 0, \"e\": 1, \"i\": {\"x\":[]}}]}}}]}";
+
+ SkMemoryStream stream(json, strlen(json));
+
+ // Passes if parsing doesn't crash.
+ auto animation = Animation::Make(&stream);
+}
+
+DEF_TEST(Skottie_Annotations, reporter) {
+ static constexpr char json[] = R"({
+ "v": "5.2.1",
+ "w": 100,
+ "h": 100,
+ "fr": 10,
+ "ip": 0,
+ "op": 100,
+ "layers": [
+ {
+ "ty": 1,
+ "ind": 0,
+ "ip": 0,
+ "op": 1,
+ "ks": {
+ "o": { "a": 0, "k": 50 }
+ },
+ "sw": 100,
+ "sh": 100,
+ "sc": "#ffffff"
+ }
+ ],
+ "markers": [
+ {
+ "cm": "marker_1",
+ "dr": 25,
+ "tm": 25
+ },
+ {
+ "cm": "marker_2",
+ "dr": 0,
+ "tm": 75
+ }
+ ]
+ })";
+
+ class TestMarkerObserver final : public MarkerObserver {
+ public:
+ void onMarker(const char name[], float t0, float t1) override {
+ fMarkers.push_back(std::make_tuple(name, t0, t1));
+ }
+
+ std::vector<std::tuple<std::string, float, float>> fMarkers;
+ };
+
+ SkMemoryStream stream(json, strlen(json));
+ auto observer = sk_make_sp<TestMarkerObserver>();
+
+ auto animation = skottie::Animation::Builder()
+ .setMarkerObserver(observer)
+ .make(&stream);
+
+ REPORTER_ASSERT(reporter, animation);
+ REPORTER_ASSERT(reporter, animation->duration() == 10);
+ REPORTER_ASSERT(reporter, animation->inPoint() == 0.0);
+ REPORTER_ASSERT(reporter, animation->outPoint() == 100.0);
+
+ REPORTER_ASSERT(reporter, observer->fMarkers.size() == 2ul);
+ REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[0]) == "marker_1");
+ REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[0]) == 0.25f);
+ REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[0]) == 0.50f);
+ REPORTER_ASSERT(reporter, std::get<0>(observer->fMarkers[1]) == "marker_2");
+ REPORTER_ASSERT(reporter, std::get<1>(observer->fMarkers[1]) == 0.75f);
+ REPORTER_ASSERT(reporter, std::get<2>(observer->fMarkers[1]) == 0.75f);
+}
+
+DEF_TEST(Skottie_Image_Loading, reporter) {
+ class TestResourceProvider final : public skresources::ResourceProvider {
+ public:
+ TestResourceProvider(sk_sp<skresources::ImageAsset> single_asset,
+ sk_sp<skresources::ImageAsset> multi_asset)
+ : fSingleFrameAsset(std::move(single_asset))
+ , fMultiFrameAsset (std::move( multi_asset)) {}
+
+ private:
+ sk_sp<ImageAsset> loadImageAsset(const char path[],
+ const char name[],
+ const char id[]) const override {
+ return strcmp(id, "single_frame")
+ ? fMultiFrameAsset
+ : fSingleFrameAsset;
+ }
+
+ const sk_sp<skresources::ImageAsset> fSingleFrameAsset,
+ fMultiFrameAsset;
+ };
+
+ auto make_animation = [&reporter] (sk_sp<skresources::ImageAsset> single_asset,
+ sk_sp<skresources::ImageAsset> multi_asset,
+ bool deferred_image_loading) {
+ static constexpr char json[] = R"({
+ "v": "5.2.1",
+ "w": 100,
+ "h": 100,
+ "fr": 10,
+ "ip": 0,
+ "op": 100,
+ "assets": [
+ {
+ "id": "single_frame",
+ "p" : "single_frame.png",
+ "u" : "images/",
+ "w" : 500,
+ "h" : 500
+ },
+ {
+ "id": "multi_frame",
+ "p" : "multi_frame.png",
+ "u" : "images/",
+ "w" : 500,
+ "h" : 500
+ }
+ ],
+ "layers": [
+ {
+ "ty": 2,
+ "refId": "single_frame",
+ "ind": 0,
+ "ip": 0,
+ "op": 100,
+ "ks": {}
+ },
+ {
+ "ty": 2,
+ "refId": "multi_frame",
+ "ind": 1,
+ "ip": 0,
+ "op": 100,
+ "ks": {}
+ }
+ ]
+ })";
+
+ SkMemoryStream stream(json, strlen(json));
+
+ const auto flags = deferred_image_loading
+ ? static_cast<uint32_t>(skottie::Animation::Builder::kDeferImageLoading)
+ : 0;
+ auto animation =
+ skottie::Animation::Builder(flags)
+ .setResourceProvider(sk_make_sp<TestResourceProvider>(std::move(single_asset),
+ std::move( multi_asset)))
+ .make(&stream);
+
+ REPORTER_ASSERT(reporter, animation);
+
+ return animation;
+ };
+
+ class TestAsset final : public skresources::ImageAsset {
+ public:
+ explicit TestAsset(bool multi_frame) : fMultiFrame(multi_frame) {}
+
+ const std::vector<float>& requestedFrames() const { return fRequestedFrames; }
+
+ private:
+ bool isMultiFrame() override { return fMultiFrame; }
+
+ sk_sp<SkImage> getFrame(float t) override {
+ fRequestedFrames.push_back(t);
+
+ return SkSurfaces::Raster(SkImageInfo::MakeN32Premul(10, 10))->makeImageSnapshot();
+ }
+
+ const bool fMultiFrame;
+
+ std::vector<float> fRequestedFrames;
+ };
+
+ {
+ auto single_asset = sk_make_sp<TestAsset>(false),
+ multi_asset = sk_make_sp<TestAsset>(true);
+
+ // Default image loading: single-frame images are loaded upfront, multi-frame images are
+ // loaded on-demand.
+ auto animation = make_animation(single_asset, multi_asset, false);
+
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 0);
+ REPORTER_ASSERT(reporter, SkScalarNearlyZero(single_asset->requestedFrames()[0]));
+
+ animation->seekFrameTime(1);
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[0], 1));
+
+ animation->seekFrameTime(2);
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 2);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
+ }
+
+ {
+ auto single_asset = sk_make_sp<TestAsset>(false),
+ multi_asset = sk_make_sp<TestAsset>(true);
+
+ // Deferred image loading: both single-frame and multi-frame images are loaded on-demand.
+ auto animation = make_animation(single_asset, multi_asset, true);
+
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 0);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 0);
+
+ animation->seekFrameTime(1);
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(single_asset->requestedFrames()[0], 1));
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual (multi_asset->requestedFrames()[0], 1));
+
+ animation->seekFrameTime(2);
+ REPORTER_ASSERT(reporter, single_asset->requestedFrames().size() == 1);
+ REPORTER_ASSERT(reporter, multi_asset->requestedFrames().size() == 2);
+ REPORTER_ASSERT(reporter, SkScalarNearlyEqual(multi_asset->requestedFrames()[1], 2));
+ }
+}
+
+DEF_TEST(Skottie_Layer_NoType, r) {
+ static constexpr char json[] =
+ R"({
+ "v": "5.2.1",
+ "w": 100,
+ "h": 100,
+ "fr": 10,
+ "ip": 0,
+ "op": 100,
+ "layers": [
+ {
+ "ind": 0,
+ "ip": 0,
+ "op": 100,
+ "ks": {}
+ }
+ ]
+ })";
+
+ SkMemoryStream stream(json, strlen(json));
+ auto anim = Animation::Make(&stream);
+
+ // passes if we don't crash
+ REPORTER_ASSERT(r, anim);
+}
+
+DEF_TEST(Skottie_Gradient_InvalidCount, r) {
+ static constexpr char json[] = R"({
+ "v": "5.12.0",
+ "fr": 60,
+ "ip": 0,
+ "op": 1,
+ "w": 64,
+ "h": 64,
+ "layers": [
+ {
+ "ind": 1,
+ "ty": 4,
+ "sr": 1,
+ "ks": {},
+ "shapes": [
+ {
+ "ty": "rc",
+ "d": 1,
+ "p": { "a": 0, "k": [0, 0] },
+ "s": { "a": 0, "k": [32, 32] },
+ "r": { "a": 0, "k": 0 }
+ },
+ {
... (truncated)