Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/framework/mlt_animation.c
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include "mlt_properties.h"
#include "mlt_tokeniser.h"

#include <ctype.h>
#include <float.h>
#include <math.h>
#include <stdio.h>
Expand Down Expand Up @@ -286,6 +287,29 @@ int mlt_animation_parse(
if (!value || !strcmp(value, ""))
continue;

// If the value does not start with a frame, time, or timecode, abort
const char *begin = value;
const char *end = value + strlen(value) - 1;
while (begin < end && isspace((unsigned char) *begin))
++begin;
int digit_found = 0;
int equal_found = 0;
for (const char *p = begin; p <= end; ++p) {
if (isdigit(p[0])) {
digit_found = 1;
continue;
}
if (p[0] == ':' || p[0] == '.' || p[0] == ',' || p[0] == '-' || p[0] == '+')
continue;
if (p[0] == '=' || p[1] == '=')
equal_found = 1;
// Any other character is unexpected and invalid
break;
}
if (!digit_found || !equal_found)
// Unexpected characters found in the time field
break;

// Reset item
item.frame = item.is_key = 0;

Expand Down
107 changes: 103 additions & 4 deletions src/framework/mlt_property.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#include "mlt_animation.h"
#include "mlt_properties.h"

#include <ctype.h>
#include <float.h>
#include <locale.h>
#include <math.h>
Expand Down Expand Up @@ -119,8 +120,9 @@ mlt_property mlt_property_init()

static void clear_property(mlt_property self)
{
// Special case data handling
if (self->types & mlt_prop_data && self->destructor != NULL)
// Special case data handling (destructor may be set even without mlt_prop_data,
// e.g. for the unquoted-string cache used by mlt_property_anim_get_string).
if (self->destructor != NULL)
self->destructor(self->data);

// Special case string handling
Expand Down Expand Up @@ -1563,8 +1565,29 @@ char *mlt_property_anim_get_string(
mlt_property_close(item.property);
pthread_mutex_unlock(&self->mutex);
} else {
const char *raw = mlt_property_get_string_l(self, locale);
if (raw && raw[0] == '"') {
size_t len = strlen(raw);
if (len >= 2 && raw[len - 1] == '"') {
// The string is wrapped in double-quotes to prevent it from
// being interpreted as animation keyframes. Strip the quotes
// and cache the result in the data field so prop_string (with
// its quotes) is never modified and is_anim() stays correct.
char *unquoted = malloc(len - 1);
memcpy(unquoted, raw + 1, len - 2);
unquoted[len - 2] = '\0';
if (self->destructor)
self->destructor(self->data);
self->data = unquoted;
self->destructor = free;
result = (char *) self->data;
} else {
result = (char *) raw;
}
} else {
result = (char *) raw;
}
pthread_mutex_unlock(&self->mutex);
result = mlt_property_get_string_l(self, locale);
}
return result;
}
Expand Down Expand Up @@ -2106,18 +2129,94 @@ mlt_properties mlt_property_get_properties(mlt_property self)
return properties;
}

static int is_keyframe_type_marker(char c)
{
switch (c) {
case '|':
case '!':
case '~':
case '$':
case '-':
return 1;
default:
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'D');
}
}

static int is_keyframe_token(const char *begin, const char *end)
{
while (begin < end && isspace((unsigned char) *begin))
++begin;
while (end > begin && isspace((unsigned char) end[-1]))
--end;
if (begin >= end)
return 0;

if (end - begin > 1 && is_keyframe_type_marker(end[-1]))
--end;

int has_digit = 0;
int trailing_unit_seen = 0;
for (const char *p = begin; p < end; ++p) {
unsigned char c = (unsigned char) *p;
if (isdigit(c)) {
has_digit = 1;
continue;
}
if (c == ':' || c == '.' || c == ',' || c == '-' || c == '+')
continue;
if (isalpha(c) && p == end - 1 && !trailing_unit_seen && has_digit) {
trailing_unit_seen = 1;
continue;
}
return 0;
}
return has_digit;
}

/** Check if a property is animated.
*
* This is not a thread-safe function because it is used internally by
* mlt_property_s under a lock. However, external callers should protect it.
*
* A property counts as animated when it already has an animation object or
* when its serialized string contains at least one unquoted keyframe item of
* the form key=value, where key looks like a frame or time token. This also
* accepts the optional single-character keyframe-type marker that serialized
* animations place immediately before '='.
*
* This intentionally does not treat every '=' as animation syntax. Plain
* strings may legitimately contain '=' or ';' characters, and those continue
* to work as ordinary string values. Quoted string payloads inside animation
* strings also continue to work: text between double quotes is ignored while
* scanning for separators, matching the documented requirement to quote string
* values that contain animation delimiters.
*
* \public \memberof mlt_property_s
* \param self a property
* \return true if the property is animated
*/

int mlt_property_is_anim(mlt_property self)
{
return self->animation || (self->prop_string && strchr(self->prop_string, '='));
if (self->animation)
return 1;
if (!self->prop_string)
return 0;

const char *token_start = self->prop_string;
int in_quotes = 0;
for (const char *p = self->prop_string; *p; ++p) {
if (*p == '"' && (p == self->prop_string || p[-1] != '\\')) {
in_quotes = !in_quotes;
continue;
}
if (!in_quotes && *p == '=')
return is_keyframe_token(token_start, p);
if (!in_quotes && *p == ';')
token_start = p + 1;
Comment on lines +2214 to +2217

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with this. "foo=bar;50=100" should not be an animation. What does "foo=bar" mean? is it just junk that should be ignored? Not a bug.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. It should definitely return immediately if false, and "foo" is not a valid time value--short circuit. But this does not continue validating items if is_keyframe_token() is true. IOW, does `mlt_property_is_anim("50=100;foo=bar") return true? I consider that an animation even though it is not entirely valid. If it walks and talks like an animation; it is an animation. OTOH something like "Love = everything; you = my world" is not an animation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"50=100;foo=bar"

I could add a test for that if we want it to work. In that case, "foo=bar" will just be considered junk and the end of the animation parsing. For example, "50=100;foo=bar;60=120" would be the same as "50=100" because the "foo=bar" would cause the parsing to abort. In addition to the extra test for that, I would also update the documentation to explain:

One leading valid key/value pair is enough to consider the animation valid. However, if invalid key/value pairs are encountered during parsing, the parsing will abort and only the initial valid keyframes will be interpreted. If the first characters of the string do not make up a valid key/value pair, then the entire string is not interpreted to be an animation and is considered a string.

Does that seem good?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"50=100;foo=bar;60=120" would be the same as "50=100

That is my expectation. I think a unit test is desirable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This idea conflicts with an existing unit test:

https://github.com/mltframework/mlt/blob/master/src/tests/test_animation/test_animation.cpp#L355

The current behavior for this test string is that the parser assigns the frame to 0 and uses that if a frame/time can not be parsed from the string token.

So, with the current behavior, 50=100;foo=bar;60=120 becomes
0=foo=bar
50=100
60=120

It is easy to change the existing test. Do we want to do that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spirit of that sub-test is "anim strings may contain equal signs if quoted" and your change keeps that. That test is an extreme outlier case. Change the test to add a frame key to resolve it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
return 0;
}

/** Check if a property is a color.
Expand Down
33 changes: 32 additions & 1 deletion src/tests/test_animation/test_animation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,43 @@ private Q_SLOTS:
// Quotes are removed when using the anim getter.
QCOMPARE(p.anim_get("foo", 0), "50=100; 60=60; 100=0");
// Anim strings may contain delimiters and equal signs if quoted.
p.set("foo", "50=100; 60=\"60; 100=0\";\"hello=world\"");
p.set("foo", "50=100; 60=\"60; 100=0\";0=\"hello=world\"");
QVERIFY(p.is_anim("foo"));
QCOMPARE(p.anim_get("foo", 0), "hello=world");
QCOMPARE(p.anim_get("foo", 50), "100");
QCOMPARE(p.anim_get("foo", 60), "60; 100=0");
}

void UrlWithEqualSignIsNotAnimation()
{
Properties p;
// A URL containing '=' in query parameters must not be treated as animation.
const char *url = "https://example.com/video?token=abc123&quality=high";
p.set("foo", url);
QVERIFY(!p.is_anim("foo"));
QCOMPARE(p.get("foo"), url);
QCOMPARE(p.anim_get("foo", 0), url);
// Verify the property is not animated.
p.anim_get("foo", 0);
QVERIFY(!p.get_animation("foo"));
}

void ParseUntilJunk()
{
Properties p;
// "foo=bar" is junk and parsing will stop after it
const char *str = "50=100;foo=bar;60=120";
p.set("foo", str);
QVERIFY(p.is_anim("foo"));
QCOMPARE(p.get("foo"), str);
QCOMPARE(p.anim_get("foo", 50), "100");
QCOMPARE(p.anim_get("foo", 60), "100"); // NOT 120
// Verify the property is animated and only has one key.
Animation a = p.get_animation("foo");
QVERIFY(a.is_valid());
QCOMPARE(a.key_count(), 1);
}

void ShiftFramesPositive()
{
Properties p;
Expand Down
Loading