1 // Copyright 2014 The PDFium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 // Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
6
7 #include "fxjs/cjs_util.h"
8
9 #include <math.h>
10 #include <time.h>
11
12 #include <algorithm>
13 #include <string>
14 #include <vector>
15
16 #include "build/build_config.h"
17 #include "core/fxcrt/check_op.h"
18 #include "core/fxcrt/compiler_specific.h"
19 #include "core/fxcrt/fx_extension.h"
20 #include "core/fxcrt/span.h"
21 #include "fxjs/cjs_event_context.h"
22 #include "fxjs/cjs_object.h"
23 #include "fxjs/cjs_publicmethods.h"
24 #include "fxjs/cjs_runtime.h"
25 #include "fxjs/fx_date_helpers.h"
26 #include "fxjs/fxv8.h"
27 #include "fxjs/js_define.h"
28 #include "fxjs/js_resources.h"
29 #include "v8/include/v8-date.h"
30
31 #if BUILDFLAG(IS_ANDROID)
32 #include <ctype.h>
33 #endif
34
35 namespace {
36
37 // Map PDF-style directives to equivalent wcsftime directives. Not
38 // all have direct equivalents, though.
39 struct TbConvert {
40 const wchar_t* lpszJSMark;
41 const wchar_t* lpszCppMark;
42 };
43
44 // Map PDF-style directives lacking direct wcsftime directives to
45 // the value with which they will be replaced.
46 struct TbConvertAdditional {
47 wchar_t js_mark;
48 int value;
49 };
50
51 const TbConvert kTbConvertTable[] = {
52 {L"mmmm", L"%B"}, {L"mmm", L"%b"}, {L"mm", L"%m"}, {L"dddd", L"%A"},
53 {L"ddd", L"%a"}, {L"dd", L"%d"}, {L"yyyy", L"%Y"}, {L"yy", L"%y"},
54 {L"HH", L"%H"}, {L"hh", L"%I"}, {L"MM", L"%M"}, {L"ss", L"%S"},
55 {L"TT", L"%p"},
56 #if BUILDFLAG(IS_WIN)
57 {L"tt", L"%p"}, {L"h", L"%#I"},
58 #else
59 {L"tt", L"%P"}, {L"h", L"%l"},
60 #endif
61 };
62
63 enum CaseMode { kPreserveCase, kUpperCase, kLowerCase };
64
TranslateCase(wchar_t input,CaseMode eMode)65 wchar_t TranslateCase(wchar_t input, CaseMode eMode) {
66 if (eMode == kLowerCase && FXSYS_iswupper(input))
67 return input | 0x20;
68 if (eMode == kUpperCase && FXSYS_iswlower(input))
69 return input & ~0x20;
70 return input;
71 }
72
73 } // namespace
74
75 const JSMethodSpec CJS_Util::MethodSpecs[] = {
76 {"printd", printd_static},
77 {"printf", printf_static},
78 {"printx", printx_static},
79 {"scand", scand_static},
80 {"byteToChar", byteToChar_static}};
81
82 uint32_t CJS_Util::ObjDefnID = 0;
83 const char CJS_Util::kName[] = "util";
84
85 // static
GetObjDefnID()86 uint32_t CJS_Util::GetObjDefnID() {
87 return ObjDefnID;
88 }
89
90 // static
DefineJSObjects(CFXJS_Engine * pEngine)91 void CJS_Util::DefineJSObjects(CFXJS_Engine* pEngine) {
92 ObjDefnID = pEngine->DefineObj(CJS_Util::kName, FXJSOBJTYPE_STATIC,
93 JSConstructor<CJS_Util>, JSDestructor);
94 DefineMethods(pEngine, ObjDefnID, MethodSpecs);
95 }
96
CJS_Util(v8::Local<v8::Object> pObject,CJS_Runtime * pRuntime)97 CJS_Util::CJS_Util(v8::Local<v8::Object> pObject, CJS_Runtime* pRuntime)
98 : CJS_Object(pObject, pRuntime) {}
99
100 CJS_Util::~CJS_Util() = default;
101
printf(CJS_Runtime * pRuntime,pdfium::span<v8::Local<v8::Value>> params)102 CJS_Result CJS_Util::printf(CJS_Runtime* pRuntime,
103 pdfium::span<v8::Local<v8::Value>> params) {
104 const size_t num_params = params.size();
105 if (num_params < 1)
106 return CJS_Result::Failure(JSMessage::kParamError);
107
108 // Use 'S' as a sentinel to ensure we always have some text before the first
109 // format specifier.
110 WideString unsafe_fmt_string = L'S' + pRuntime->ToWideString(params[0]);
111 std::vector<WideString> unsafe_conversion_specifiers;
112
113 {
114 size_t offset = 0;
115 while (true) {
116 std::optional<size_t> offset_end =
117 unsafe_fmt_string.Find(L"%", offset + 1);
118 if (!offset_end.has_value()) {
119 unsafe_conversion_specifiers.push_back(
120 unsafe_fmt_string.Last(unsafe_fmt_string.GetLength() - offset));
121 break;
122 }
123
124 unsafe_conversion_specifiers.push_back(
125 unsafe_fmt_string.Substr(offset, offset_end.value() - offset));
126 offset = offset_end.value();
127 }
128 }
129
130 WideString result = unsafe_conversion_specifiers[0];
131 for (size_t i = 1; i < unsafe_conversion_specifiers.size(); ++i) {
132 WideString fmt = unsafe_conversion_specifiers[i];
133 if (i >= num_params) {
134 result += fmt;
135 continue;
136 }
137
138 WideString segment;
139 switch (ParseDataType(&fmt)) {
140 case DataType::kInt:
141 segment = WideString::Format(fmt.c_str(), pRuntime->ToInt32(params[i]));
142 break;
143 case DataType::kDouble:
144 segment =
145 WideString::Format(fmt.c_str(), pRuntime->ToDouble(params[i]));
146 break;
147 case DataType::kString:
148 segment = WideString::Format(fmt.c_str(),
149 pRuntime->ToWideString(params[i]).c_str());
150 break;
151 default:
152 segment = WideString::Format(L"%ls", fmt.c_str());
153 break;
154 }
155 result += segment;
156 }
157
158 // Remove the 'S' sentinel introduced earlier.
159 DCHECK_EQ(L'S', result[0]);
160 auto result_view = result.AsStringView();
161 return CJS_Result::Success(
162 pRuntime->NewString(result_view.Last(result_view.GetLength() - 1)));
163 }
164
printd(CJS_Runtime * pRuntime,pdfium::span<v8::Local<v8::Value>> params)165 CJS_Result CJS_Util::printd(CJS_Runtime* pRuntime,
166 pdfium::span<v8::Local<v8::Value>> params) {
167 const size_t iSize = params.size();
168 if (iSize < 2)
169 return CJS_Result::Failure(JSMessage::kParamError);
170
171 if (!fxv8::IsDate(params[1]))
172 return CJS_Result::Failure(JSMessage::kSecondParamNotDateError);
173
174 v8::Local<v8::Date> v8_date = params[1].As<v8::Date>();
175 if (v8_date.IsEmpty() || isnan(pRuntime->ToDouble(v8_date)))
176 return CJS_Result::Failure(JSMessage::kSecondParamInvalidDateError);
177
178 double date = FX_LocalTime(pRuntime->ToDouble(v8_date));
179 int year = FX_GetYearFromTime(date);
180 int month = FX_GetMonthFromTime(date) + 1; // One-based.
181 int day = FX_GetDayFromTime(date);
182 int hour = FX_GetHourFromTime(date);
183 int min = FX_GetMinFromTime(date);
184 int sec = FX_GetSecFromTime(date);
185
186 if (params[0]->IsNumber()) {
187 WideString swResult;
188 switch (pRuntime->ToInt32(params[0])) {
189 case 0:
190 swResult = WideString::Format(L"D:%04d%02d%02d%02d%02d%02d", year,
191 month, day, hour, min, sec);
192 break;
193 case 1:
194 swResult = WideString::Format(L"%04d.%02d.%02d %02d:%02d:%02d", year,
195 month, day, hour, min, sec);
196 break;
197 case 2:
198 swResult = WideString::Format(L"%04d/%02d/%02d %02d:%02d:%02d", year,
199 month, day, hour, min, sec);
200 break;
201 default:
202 return CJS_Result::Failure(JSMessage::kValueError);
203 }
204
205 return CJS_Result::Success(pRuntime->NewString(swResult.AsStringView()));
206 }
207
208 if (!params[0]->IsString())
209 return CJS_Result::Failure(JSMessage::kTypeError);
210
211 // We don't support XFAPicture at the moment.
212 if (iSize > 2 && pRuntime->ToBoolean(params[2]))
213 return CJS_Result::Failure(JSMessage::kNotSupportedError);
214
215 // Convert PDF-style format specifiers to wcsftime specifiers. Remove any
216 // pre-existing %-directives before inserting our own.
217 std::wstring cFormat = pRuntime->ToWideString(params[0]).c_str();
218 cFormat.erase(std::remove(cFormat.begin(), cFormat.end(), '%'),
219 cFormat.end());
220
221 for (const auto& conversion : kTbConvertTable) {
222 size_t nFound = 0;
223 while (true) {
224 nFound = cFormat.find(conversion.lpszJSMark, nFound);
225 if (nFound == std::wstring::npos) {
226 break;
227 }
228 cFormat.replace(nFound, wcslen(conversion.lpszJSMark),
229 conversion.lpszCppMark);
230 }
231 }
232
233 if (year < 0)
234 return CJS_Result::Failure(JSMessage::kValueError);
235
236 const TbConvertAdditional table_additional[] = {
237 {L'm', month}, {L'd', day},
238 {L'H', hour}, {L'h', hour > 12 ? hour - 12 : hour},
239 {L'M', min}, {L's', sec},
240 };
241
242 for (const auto& conversion : table_additional) {
243 size_t nFound = 0;
244 while (true) {
245 nFound = cFormat.find(conversion.js_mark, nFound);
246 if (nFound == std::wstring::npos) {
247 break;
248 }
249 if (nFound != 0 && cFormat[nFound - 1] == L'%') {
250 ++nFound;
251 continue;
252 }
253 cFormat.replace(nFound, 1,
254 WideString::FormatInteger(conversion.value).c_str());
255 }
256 }
257
258 struct tm time = {};
259 time.tm_year = year - 1900;
260 time.tm_mon = month - 1;
261 time.tm_mday = day;
262 time.tm_hour = hour;
263 time.tm_min = min;
264 time.tm_sec = sec;
265
266 wchar_t buf[64] = {};
267 UNSAFE_TODO(FXSYS_wcsftime(buf, 64, cFormat.c_str(), &time));
268 cFormat = buf;
269 return CJS_Result::Success(pRuntime->NewString(cFormat.c_str()));
270 }
271
printx(CJS_Runtime * pRuntime,pdfium::span<v8::Local<v8::Value>> params)272 CJS_Result CJS_Util::printx(CJS_Runtime* pRuntime,
273 pdfium::span<v8::Local<v8::Value>> params) {
274 if (params.size() < 2)
275 return CJS_Result::Failure(JSMessage::kParamError);
276
277 return CJS_Result::Success(
278 pRuntime->NewString(StringPrintx(pRuntime->ToWideString(params[0]),
279 pRuntime->ToWideString(params[1]))
280 .AsStringView()));
281 }
282
283 // static
StringPrintx(const WideString & wsFormat,const WideString & wsSource)284 WideString CJS_Util::StringPrintx(const WideString& wsFormat,
285 const WideString& wsSource) {
286 WideString wsResult;
287 wsResult.Reserve(wsFormat.GetLength());
288 size_t iSourceIdx = 0;
289 size_t iFormatIdx = 0;
290 CaseMode eCaseMode = kPreserveCase;
291 bool bEscaped = false;
292 while (iFormatIdx < wsFormat.GetLength()) {
293 if (bEscaped) {
294 bEscaped = false;
295 wsResult += wsFormat[iFormatIdx];
296 ++iFormatIdx;
297 continue;
298 }
299 switch (wsFormat[iFormatIdx]) {
300 case '\\': {
301 bEscaped = true;
302 ++iFormatIdx;
303 } break;
304 case '<': {
305 eCaseMode = kLowerCase;
306 ++iFormatIdx;
307 } break;
308 case '>': {
309 eCaseMode = kUpperCase;
310 ++iFormatIdx;
311 } break;
312 case '=': {
313 eCaseMode = kPreserveCase;
314 ++iFormatIdx;
315 } break;
316 case '?': {
317 if (iSourceIdx < wsSource.GetLength()) {
318 wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
319 ++iSourceIdx;
320 }
321 ++iFormatIdx;
322 } break;
323 case 'X': {
324 if (iSourceIdx < wsSource.GetLength()) {
325 if (isascii(wsSource[iSourceIdx]) && isalnum(wsSource[iSourceIdx])) {
326 wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
327 ++iFormatIdx;
328 }
329 ++iSourceIdx;
330 } else {
331 ++iFormatIdx;
332 }
333 } break;
334 case 'A': {
335 if (iSourceIdx < wsSource.GetLength()) {
336 if (isascii(wsSource[iSourceIdx]) && isalpha(wsSource[iSourceIdx])) {
337 wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
338 ++iFormatIdx;
339 }
340 ++iSourceIdx;
341 } else {
342 ++iFormatIdx;
343 }
344 } break;
345 case '9': {
346 if (iSourceIdx < wsSource.GetLength()) {
347 if (FXSYS_IsDecimalDigit(wsSource[iSourceIdx])) {
348 wsResult += wsSource[iSourceIdx];
349 ++iFormatIdx;
350 }
351 ++iSourceIdx;
352 } else {
353 ++iFormatIdx;
354 }
355 } break;
356 case '*': {
357 if (iSourceIdx < wsSource.GetLength()) {
358 wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
359 ++iSourceIdx;
360 } else {
361 ++iFormatIdx;
362 }
363 } break;
364 default: {
365 wsResult += wsFormat[iFormatIdx];
366 ++iFormatIdx;
367 } break;
368 }
369 }
370 return wsResult;
371 }
372
scand(CJS_Runtime * pRuntime,pdfium::span<v8::Local<v8::Value>> params)373 CJS_Result CJS_Util::scand(CJS_Runtime* pRuntime,
374 pdfium::span<v8::Local<v8::Value>> params) {
375 if (params.size() < 2)
376 return CJS_Result::Failure(JSMessage::kParamError);
377
378 WideString sFormat = pRuntime->ToWideString(params[0]);
379 WideString sDate = pRuntime->ToWideString(params[1]);
380 double dDate = FX_GetDateTime();
381 if (sDate.GetLength() > 0)
382 dDate = CJS_PublicMethods::ParseDateUsingFormat(pRuntime->GetIsolate(),
383 sDate, sFormat, nullptr);
384 if (isnan(dDate))
385 return CJS_Result::Success(pRuntime->NewUndefined());
386
387 return CJS_Result::Success(pRuntime->NewDate(dDate));
388 }
389
byteToChar(CJS_Runtime * pRuntime,pdfium::span<v8::Local<v8::Value>> params)390 CJS_Result CJS_Util::byteToChar(CJS_Runtime* pRuntime,
391 pdfium::span<v8::Local<v8::Value>> params) {
392 if (params.size() < 1)
393 return CJS_Result::Failure(JSMessage::kParamError);
394
395 int arg = pRuntime->ToInt32(params[0]);
396 if (arg < 0 || arg > 255)
397 return CJS_Result::Failure(JSMessage::kValueError);
398
399 WideString wStr(static_cast<wchar_t>(arg));
400 return CJS_Result::Success(pRuntime->NewString(wStr.AsStringView()));
401 }
402
403 // static
ParseDataType(WideString * sFormat)404 CJS_Util::DataType CJS_Util::ParseDataType(WideString* sFormat) {
405 enum State { kBefore, kFlags, kWidth, kPrecision, kSpecifier, kAfter };
406
407 DataType result = DataType::kInvalid;
408 State state = kBefore;
409 size_t precision_digits = 0;
410 size_t i = 0;
411 while (i < sFormat->GetLength()) {
412 wchar_t c = (*sFormat)[i];
413 switch (state) {
414 case kBefore:
415 if (c == L'%')
416 state = kFlags;
417 break;
418 case kFlags:
419 if (c == L'+' || c == L'-' || c == L'#' || c == L' ') {
420 // Stay in same state.
421 } else {
422 state = kWidth;
423 continue; // Re-process same character.
424 }
425 break;
426 case kWidth:
427 if (c == L'*')
428 return DataType::kInvalid;
429 if (FXSYS_IsDecimalDigit(c)) {
430 // Stay in same state.
431 } else if (c == L'.') {
432 state = kPrecision;
433 } else {
434 state = kSpecifier;
435 continue; // Re-process same character.
436 }
437 break;
438 case kPrecision:
439 if (c == L'*')
440 return DataType::kInvalid;
441 if (FXSYS_IsDecimalDigit(c)) {
442 // Stay in same state.
443 ++precision_digits;
444 } else {
445 state = kSpecifier;
446 continue; // Re-process same character.
447 }
448 break;
449 case kSpecifier:
450 if (c == L'c' || c == L'C' || c == L'd' || c == L'i' || c == L'o' ||
451 c == L'u' || c == L'x' || c == L'X') {
452 result = DataType::kInt;
453 } else if (c == L'e' || c == L'E' || c == L'f' || c == L'g' ||
454 c == L'G') {
455 result = DataType::kDouble;
456 } else if (c == L's' || c == L'S') {
457 // Map s to S since we always deal internally with wchar_t strings.
458 // TODO(tsepez): Probably 100% borked. %S is not a standard
459 // conversion.
460 sFormat->SetAt(i, L'S');
461 result = DataType::kString;
462 } else {
463 return DataType::kInvalid;
464 }
465 state = kAfter;
466 break;
467 case kAfter:
468 if (c == L'%')
469 return DataType::kInvalid;
470 // Stay in same state until string exhausted.
471 break;
472 }
473 ++i;
474 }
475 // See https://crbug.com/740166
476 if (result == DataType::kInt && precision_digits > 2)
477 return DataType::kInvalid;
478
479 return result;
480 }
481