Skip to content

Commit 78941dd

Browse files
authored
Fix parsing of time_strings lacking leading zeroes (#1297)
Fixes #1293 fixes parsing of time strings without leading zeroes. enforces that a negative sign can only appear in the left most position implementation does not allocate memory or copy strings implementation does not allow exponential notation and other things that std does allow but are inappropriate for time strings compatible with strings produced by ffprobe associated tests adds C based tests corresponding to the existing Python based tests.
1 parent 64b0e31 commit 78941dd

File tree

4 files changed

+279
-38
lines changed

4 files changed

+279
-38
lines changed

src/opentime/rationalTime.cpp

Lines changed: 186 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,103 @@ is_dropframe_rate(double rate)
9494
return std::find(b, e, rate) != e;
9595
}
9696

97+
static bool
98+
parseFloat(char const* pCurr, char const* pEnd, bool allow_negative, double* result)
99+
{
100+
if (pCurr >= pEnd || !pCurr)
101+
{
102+
*result = 0.0;
103+
return false;
104+
}
105+
106+
double ret = 0.0;
107+
double sign = 1.0;
108+
109+
if (*pCurr == '+')
110+
{
111+
++pCurr;
112+
}
113+
else if (*pCurr == '-')
114+
{
115+
if (!allow_negative)
116+
{
117+
*result = 0.0;
118+
return false;
119+
}
120+
sign = -1.0;
121+
++pCurr;
122+
}
123+
124+
// get integer part
125+
//
126+
// Note that uint64_t is used because overflow is well defined for
127+
// unsigned integers, but it is undefined behavior for signed integers,
128+
// and floating point values are couched in the specification with
129+
// the caveat that an implementation may be IEEE-754 compliant, or only
130+
// partially compliant.
131+
//
132+
uint64_t uintPart = 0;
133+
while (pCurr < pEnd)
134+
{
135+
char c = *pCurr;
136+
if (c < '0' || c > '9')
137+
{
138+
break;
139+
}
140+
uint64_t accumulated = uintPart * 10 + c - '0';
141+
if (accumulated < uintPart)
142+
{
143+
// if there are too many digits, resulting in an overflow, fail
144+
*result = 0.0;
145+
return false;
146+
}
147+
uintPart = accumulated;
148+
++pCurr;
149+
}
150+
151+
ret = static_cast<double>(uintPart);
152+
if (uintPart != static_cast<uint64_t>(ret))
153+
{
154+
// if the double cannot be casted precisely back to uint64_t, fail
155+
// A double has 15 digits of precision, but a uint64_t can encode more.
156+
*result = 0.0;
157+
return false;
158+
}
159+
160+
// check for end of string or delimiter
161+
if (pCurr == pEnd || *pCurr == '\0')
162+
{
163+
*result = sign * ret;
164+
return true;
165+
}
166+
167+
// if the next character is not a decimal point, the string is malformed.
168+
if (*pCurr != '.')
169+
{
170+
*result = 0.0; // zero consistent with earlier error condition
171+
return false;
172+
}
173+
174+
++pCurr; // skip decimal
175+
176+
double position_scale = 0.1;
177+
while (pCurr < pEnd)
178+
{
179+
char c = *pCurr;
180+
if (c < '0' || c > '9')
181+
{
182+
break;
183+
}
184+
ret = ret + static_cast<double>(c - '0') * position_scale;
185+
++pCurr;
186+
position_scale *= 0.1;
187+
}
188+
189+
*result = sign * ret;
190+
return true;
191+
}
192+
193+
97194
RationalTime
98195
RationalTime::from_timecode(
99196
std::string const& timecode, double rate, ErrorStatus* error_status)
@@ -205,55 +302,109 @@ RationalTime::from_timecode(
205302
return RationalTime{ double(value), rate };
206303
}
207304

305+
static void
306+
set_error(std::string const& time_string,
307+
ErrorStatus::Outcome code,
308+
ErrorStatus* err)
309+
{
310+
if (err) {
311+
*err = ErrorStatus(
312+
code,
313+
string_printf(
314+
"Error: '%s' - %s",
315+
time_string.c_str(),
316+
ErrorStatus::outcome_to_string(code).c_str()));
317+
}
318+
}
319+
208320
RationalTime
209321
RationalTime::from_time_string(
210322
std::string const& time_string, double rate, ErrorStatus* error_status)
211323
{
212-
if (!RationalTime::is_valid_timecode_rate(rate))
324+
if (!RationalTime::is_valid_timecode_rate(rate))
213325
{
214-
if (error_status)
215-
{
216-
*error_status = ErrorStatus(ErrorStatus::INVALID_TIMECODE_RATE);
217-
}
326+
set_error(time_string, ErrorStatus::INVALID_TIMECODE_RATE, error_status);
218327
return RationalTime::_invalid_time;
219328
}
220329

221-
std::vector<std::string> fields(3, std::string());
222-
223-
// split the fields
224-
int last_pos = 0;
225-
226-
for (int i = 0; i < 2; i++)
330+
const char* start = time_string.data();
331+
const char* end = start + time_string.length();
332+
char* current = const_cast<char*>(end);
333+
char* parse_end = current;
334+
char* prev_parse_end = current;
335+
336+
double power[3] = {
337+
1.0, // seconds
338+
60.0, // minutes
339+
3600.0 // hours
340+
};
341+
342+
double accumulator = 0.0;
343+
int radix = 0;
344+
while (start <= current)
227345
{
228-
fields[i] = time_string.substr(last_pos, 2);
229-
last_pos = last_pos + 3;
230-
}
231-
232-
fields[2] = time_string.substr(last_pos, time_string.length());
233-
234-
double hours, minutes, seconds;
346+
if (*current == ':')
347+
{
348+
parse_end = current + 1;
349+
char c = *parse_end;
350+
if (c != '\0' && c != ':')
351+
{
352+
if (c< '0' || c > '9')
353+
{
354+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
355+
return RationalTime::_invalid_time;
356+
}
357+
double val = 0.0;
358+
if (!parseFloat(parse_end, prev_parse_end + 1, false, &val))
359+
{
360+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
361+
return RationalTime::_invalid_time;
362+
}
363+
prev_parse_end = nullptr;
364+
if (radix < 2 && val >= 60.0)
365+
{
366+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
367+
return RationalTime::_invalid_time;
368+
}
369+
accumulator += val * power[radix];
370+
}
371+
++radix;
372+
if (radix == sizeof(power) / sizeof(power[0]))
373+
{
374+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
375+
return RationalTime::_invalid_time;
376+
}
377+
}
378+
else if (current < prev_parse_end &&
379+
(*current < '0' || *current > '9') &&
380+
*current != '.')
381+
{
382+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
383+
return RationalTime::_invalid_time;
384+
}
235385

236-
try
237-
{
238-
hours = std::stod(fields[0]);
239-
minutes = std::stod(fields[1]);
240-
seconds = std::stod(fields[2]);
241-
}
242-
catch (std::exception const& e)
243-
{
244-
if (error_status)
386+
if (start == current)
245387
{
246-
*error_status = ErrorStatus(
247-
ErrorStatus::INVALID_TIME_STRING,
248-
string_printf(
249-
"Input time string '%s' is an invalid time string",
250-
time_string.c_str()));
388+
if (prev_parse_end)
389+
{
390+
double val = 0.0;
391+
if (!parseFloat(start, prev_parse_end + 1, true, &val))
392+
{
393+
set_error(time_string, ErrorStatus::INVALID_TIME_STRING, error_status);
394+
return RationalTime::_invalid_time;
395+
}
396+
accumulator += val * power[radix];
397+
}
398+
break;
399+
}
400+
--current;
401+
if (!prev_parse_end)
402+
{
403+
prev_parse_end = current;
251404
}
252-
return RationalTime::_invalid_time;
253405
}
254406

255-
return from_seconds(seconds + minutes * 60 + hours * 60 * 60)
256-
.rescaled_to(rate);
407+
return from_seconds(accumulator).rescaled_to(rate);
257408
}
258409

259410
std::string

src/opentime/rationalTime.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,11 @@ class RationalTime
127127
std::string const& timecode,
128128
double rate,
129129
ErrorStatus* error_status = nullptr);
130+
131+
// parse a string in the form
132+
// hours:minutes:seconds
133+
// which may have a leading negative sign. seconds may have up to
134+
// microsecond precision.
130135
static RationalTime from_time_string(
131136
std::string const& time_string,
132137
double rate,
@@ -154,9 +159,13 @@ class RationalTime
154159
return to_timecode(_rate, IsDropFrameRate::InferFromRate, error_status);
155160
}
156161

162+
// produce a string in the form
163+
// hours:minutes:seconds
164+
// which may have a leading negative sign. seconds may have up to
165+
// microsecond precision.
157166
std::string to_time_string() const;
158167

159-
RationalTime const& operator+=(RationalTime other) noexcept
168+
RationalTime const& operator+=(RationalTime other) noexcept
160169
{
161170
if (_rate < other._rate)
162171
{
@@ -170,7 +179,7 @@ class RationalTime
170179
return *this;
171180
}
172181

173-
RationalTime const& operator-=(RationalTime other) noexcept
182+
RationalTime const& operator-=(RationalTime other) noexcept
174183
{
175184
if (_rate < other._rate)
176185
{

tests/test_opentime.cpp

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,75 @@ main(int argc, char** argv)
3838
assertFalse(t1 != t3);
3939
});
4040

41+
tests.add_test("test_from_time_string", [] {
42+
std::string time_string = "0:12:04";
43+
auto t = otime::RationalTime(24 * (12 * 60 + 4), 24);
44+
auto time_obj = otime::RationalTime::from_time_string(time_string, 24);
45+
assertTrue(t.almost_equal(time_obj, 0.001));
46+
});
47+
48+
tests.add_test("test_from_time_string24", [] {
49+
std::string time_string = "00:00:00.041667";
50+
auto t = otime::RationalTime(1, 24);
51+
auto time_obj = otime::RationalTime::from_time_string(time_string, 24);
52+
assertTrue(t.almost_equal(time_obj, 0.001));
53+
time_string = "00:00:01";
54+
t = otime::RationalTime(24, 24);
55+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
56+
assertTrue(t.almost_equal(time_obj, 0.001));
57+
time_string = "00:01:00";
58+
t = otime::RationalTime(60 * 24, 24);
59+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
60+
assertTrue(t.almost_equal(time_obj, 0.001));
61+
time_string = "01:00:00";
62+
t = otime::RationalTime(60 * 60 * 24, 24);
63+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
64+
assertTrue(t.almost_equal(time_obj, 0.001));
65+
time_string = "24:00:00";
66+
t = otime::RationalTime(24 * 60 * 60 * 24, 24);
67+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
68+
assertTrue(t.almost_equal(time_obj, 0.001));
69+
time_string = "23:59:59.92";
70+
t = otime::RationalTime((23 * 60 * 60 + 59 * 60 + 59.92) * 24, 24);
71+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
72+
assertTrue(t.almost_equal(time_obj, 0.001));
73+
});
74+
75+
tests.add_test("test_from_time_string25", [] {
76+
std::string time_string = "0:12:04.929792";
77+
auto t = otime::RationalTime((12 * 60 + 4.929792) * 25, 25);
78+
auto time_obj = otime::RationalTime::from_time_string(time_string, 24);
79+
assertTrue(t.almost_equal(time_obj, 0.001));
80+
time_string = "00:00:01";
81+
t = otime::RationalTime(25, 25);
82+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
83+
assertTrue(t.almost_equal(time_obj, 0.001));
84+
time_string = "0:1";
85+
t = otime::RationalTime(25, 25);
86+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
87+
assertTrue(t.almost_equal(time_obj, 0.001));
88+
time_string = "1";
89+
t = otime::RationalTime(25, 25);
90+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
91+
assertTrue(t.almost_equal(time_obj, 0.001));
92+
time_string = "00:01:00";
93+
t = otime::RationalTime(60 * 25, 25);
94+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
95+
assertTrue(t.almost_equal(time_obj, 0.001));
96+
time_string = "01:00:00";
97+
t = otime::RationalTime(60 * 60 * 25, 25);
98+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
99+
assertTrue(t.almost_equal(time_obj, 0.001));
100+
time_string = "24:00:00";
101+
t = otime::RationalTime(24 * 60 * 60 * 25, 25);
102+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
103+
assertTrue(t.almost_equal(time_obj, 0.001));
104+
time_string = "23:59:59.92";
105+
t = otime::RationalTime((23 * 60 * 60 + 59 * 60 + 59.92) * 25, 25);
106+
time_obj = otime::RationalTime::from_time_string(time_string, 24);
107+
assertTrue(t.almost_equal(time_obj, 0.001));
108+
});
109+
41110
tests.run(argc, argv);
42111
return 0;
43112
}

tests/test_opentime.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ def test_create(self):
1717
self.assertIsNotNone(t)
1818
self.assertEqual(t.value, t_val)
1919

20+
t_val = -30.2
21+
t = otio.opentime.RationalTime(t_val)
22+
self.assertIsNotNone(t)
23+
self.assertEqual(t.value, t_val)
24+
2025
t = otio.opentime.RationalTime()
2126
self.assertEqual(t.value, 0)
2227
self.assertEqual(t.rate, 1.0)
@@ -73,7 +78,6 @@ def test_deepcopy(self):
7378
self.assertEqual(t2, otio.opentime.RationalTime(18, 24))
7479

7580
def test_base_conversion(self):
76-
7781
# from a number
7882
t = otio.opentime.RationalTime(10, 24)
7983
with self.assertRaises(TypeError):
@@ -93,6 +97,14 @@ def test_time_timecode_convert(self):
9397
t = otio.opentime.from_timecode(timecode, 24)
9498
self.assertEqual(timecode, otio.opentime.to_timecode(t))
9599

100+
def test_negative_timecode(self):
101+
with self.assertRaises(ValueError):
102+
otio.opentime.from_timecode('-01:00:13:13', 24)
103+
104+
def test_bogus_timecode(self):
105+
with self.assertRaises(ValueError):
106+
otio.opentime.from_timecode('pink elephants', 13)
107+
96108
def test_time_timecode_convert_bad_rate(self):
97109
with self.assertRaises(ValueError) as exception_manager:
98110
otio.opentime.from_timecode('01:00:13:24', 24)

0 commit comments

Comments
 (0)