pdfium/fxjs/cjs_util.cpp
Tom Sepez 937e950631 Add big scary term "Reentrant" to fxjs methods that enter JS.
Allows easier visualization of places where the JS side may
change legitimacy of previously extracted values.

-- Gemini-generated rename, no funny business (hopefully).

Change-Id: I2aeab6e4b8092a16258b3c3586cebaa34ebaf21a
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/147770
Reviewed-by: Lei Zhang <thestig@chromium.org>
Commit-Queue: Tom Sepez <tsepez@chromium.org>
2026-05-15 14:39:54 -07:00

502 lines
14 KiB
C++

// Copyright 2014 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Original code copyright 2014 Foxit Software Inc. http://www.foxitsoftware.com
#include "fxjs/cjs_util.h"
#include <math.h>
#include <time.h>
#include <algorithm>
#include <string>
#include <vector>
#include "build/build_config.h"
#include "core/fxcrt/check_op.h"
#include "core/fxcrt/compiler_specific.h"
#include "core/fxcrt/fx_extension.h"
#include "core/fxcrt/span.h"
#include "fxjs/cjs_event_context.h"
#include "fxjs/cjs_object.h"
#include "fxjs/cjs_publicmethods.h"
#include "fxjs/cjs_runtime.h"
#include "fxjs/fx_date_helpers.h"
#include "fxjs/fxv8.h"
#include "fxjs/js_define.h"
#include "fxjs/js_resources.h"
#include "v8/include/v8-date.h"
#if BUILDFLAG(IS_ANDROID)
#include <ctype.h>
#endif
namespace {
// Map PDF-style directives to equivalent wcsftime directives. Not
// all have direct equivalents, though.
struct TbConvert {
const wchar_t* lpszJSMark;
const wchar_t* lpszCppMark;
};
// Map PDF-style directives lacking direct wcsftime directives to
// the value with which they will be replaced.
struct TbConvertAdditional {
wchar_t js_mark;
int value;
};
const TbConvert kTbConvertTable[] = {
{L"mmmm", L"%B"}, {L"mmm", L"%b"}, {L"mm", L"%m"}, {L"dddd", L"%A"},
{L"ddd", L"%a"}, {L"dd", L"%d"}, {L"yyyy", L"%Y"}, {L"yy", L"%y"},
{L"HH", L"%H"}, {L"hh", L"%I"}, {L"MM", L"%M"}, {L"ss", L"%S"},
{L"TT", L"%p"},
#if BUILDFLAG(IS_WIN)
{L"tt", L"%p"}, {L"h", L"%#I"},
#else
{L"tt", L"%P"}, {L"h", L"%l"},
#endif
};
enum CaseMode { kPreserveCase, kUpperCase, kLowerCase };
wchar_t TranslateCase(wchar_t input, CaseMode eMode) {
if (eMode == kLowerCase && FXSYS_iswupper(input)) {
return input | 0x20;
}
if (eMode == kUpperCase && FXSYS_iswlower(input)) {
return input & ~0x20;
}
return input;
}
} // namespace
const JSMethodSpec CJS_Util::MethodSpecs[] = {
{"printd", printd_static},
{"printf", printf_static},
{"printx", printx_static},
{"scand", scand_static},
{"byteToChar", byteToChar_static}};
uint32_t CJS_Util::ObjDefnID = 0;
const char CJS_Util::kName[] = "util";
// static
uint32_t CJS_Util::GetObjDefnID() {
return ObjDefnID;
}
// static
void CJS_Util::DefineJSObjects(CFXJS_Engine* pEngine) {
ObjDefnID = pEngine->DefineObj(CJS_Util::kName, FXJSOBJTYPE_STATIC,
JSConstructor<CJS_Util>, JSDestructor);
DefineMethods(pEngine, ObjDefnID, MethodSpecs);
}
CJS_Util::CJS_Util(v8::Local<v8::Object> pObject, CJS_Runtime* pRuntime)
: CJS_Object(pObject, pRuntime) {}
CJS_Util::~CJS_Util() = default;
CJS_Result CJS_Util::printf(CJS_Runtime* pRuntime,
pdfium::span<v8::Local<v8::Value>> params) {
const size_t num_params = params.size();
if (num_params < 1) {
return CJS_Result::Failure(JSMessage::kParamError);
}
// Use 'S' as a sentinel to ensure we always have some text before the first
// format specifier.
WideString unsafe_fmt_string =
L'S' + pRuntime->ToWideStringReentrant(params[0]);
std::vector<WideString> unsafe_conversion_specifiers;
{
size_t offset = 0;
while (true) {
std::optional<size_t> offset_end =
unsafe_fmt_string.Find(L"%", offset + 1);
if (!offset_end.has_value()) {
unsafe_conversion_specifiers.push_back(
unsafe_fmt_string.Substr(offset));
break;
}
unsafe_conversion_specifiers.push_back(
unsafe_fmt_string.Substr(offset, offset_end.value() - offset));
offset = offset_end.value();
}
}
WideString result = unsafe_conversion_specifiers[0];
for (size_t i = 1; i < unsafe_conversion_specifiers.size(); ++i) {
WideString fmt = unsafe_conversion_specifiers[i];
if (i >= num_params) {
result += fmt;
continue;
}
WideString segment;
switch (ParseDataType(&fmt)) {
case DataType::kInt:
segment = WideString::Format(fmt.c_str(),
pRuntime->ToInt32Reentrant(params[i]));
break;
case DataType::kDouble:
segment = WideString::Format(fmt.c_str(),
pRuntime->ToDoubleReentrant(params[i]));
break;
case DataType::kString:
segment = WideString::Format(
fmt.c_str(), pRuntime->ToWideStringReentrant(params[i]).c_str());
break;
default:
segment = WideString::Format(L"%ls", fmt.c_str());
break;
}
result += segment;
}
// Remove the 'S' sentinel introduced earlier.
DCHECK_EQ(L'S', result[0]);
auto result_view = result.AsStringView();
return CJS_Result::Success(pRuntime->NewString(result_view.Substr(1)));
}
CJS_Result CJS_Util::printd(CJS_Runtime* pRuntime,
pdfium::span<v8::Local<v8::Value>> params) {
const size_t iSize = params.size();
if (iSize < 2) {
return CJS_Result::Failure(JSMessage::kParamError);
}
if (!fxv8::IsDate(params[1])) {
return CJS_Result::Failure(JSMessage::kSecondParamNotDateError);
}
v8::Local<v8::Date> v8_date = params[1].As<v8::Date>();
if (v8_date.IsEmpty() || isnan(pRuntime->ToDoubleReentrant(v8_date))) {
return CJS_Result::Failure(JSMessage::kSecondParamInvalidDateError);
}
double date = FX_LocalTime(pRuntime->ToDoubleReentrant(v8_date));
int year = FX_GetYearFromTime(date);
int month = FX_GetMonthFromTime(date) + 1; // One-based.
int day = FX_GetDayFromTime(date);
int hour = FX_GetHourFromTime(date);
int min = FX_GetMinFromTime(date);
int sec = FX_GetSecFromTime(date);
if (params[0]->IsNumber()) {
WideString swResult;
switch (pRuntime->ToInt32Reentrant(params[0])) {
case 0:
swResult = WideString::Format(L"D:%04d%02d%02d%02d%02d%02d", year,
month, day, hour, min, sec);
break;
case 1:
swResult = WideString::Format(L"%04d.%02d.%02d %02d:%02d:%02d", year,
month, day, hour, min, sec);
break;
case 2:
swResult = WideString::Format(L"%04d/%02d/%02d %02d:%02d:%02d", year,
month, day, hour, min, sec);
break;
default:
return CJS_Result::Failure(JSMessage::kValueError);
}
return CJS_Result::Success(pRuntime->NewString(swResult.AsStringView()));
}
if (!params[0]->IsString()) {
return CJS_Result::Failure(JSMessage::kTypeError);
}
// We don't support XFAPicture at the moment.
if (iSize > 2 && pRuntime->ToBooleanReentrant(params[2])) {
return CJS_Result::Failure(JSMessage::kNotSupportedError);
}
// Convert PDF-style format specifiers to wcsftime specifiers. Remove any
// pre-existing %-directives before inserting our own.
std::wstring cFormat = pRuntime->ToWideStringReentrant(params[0]).c_str();
cFormat.erase(std::remove(cFormat.begin(), cFormat.end(), '%'),
cFormat.end());
for (const auto& conversion : kTbConvertTable) {
size_t nFound = 0;
while (true) {
nFound = cFormat.find(conversion.lpszJSMark, nFound);
if (nFound == std::wstring::npos) {
break;
}
cFormat.replace(nFound, wcslen(conversion.lpszJSMark),
conversion.lpszCppMark);
}
}
if (year < 0) {
return CJS_Result::Failure(JSMessage::kValueError);
}
const TbConvertAdditional table_additional[] = {
{L'm', month}, {L'd', day},
{L'H', hour}, {L'h', hour > 12 ? hour - 12 : hour},
{L'M', min}, {L's', sec},
};
for (const auto& conversion : table_additional) {
size_t nFound = 0;
while (true) {
nFound = cFormat.find(conversion.js_mark, nFound);
if (nFound == std::wstring::npos) {
break;
}
if (nFound != 0 && cFormat[nFound - 1] == L'%') {
++nFound;
continue;
}
cFormat.replace(nFound, 1,
WideString::FormatInteger(conversion.value).c_str());
}
}
struct tm time = {};
time.tm_year = year - 1900;
time.tm_mon = month - 1;
time.tm_mday = day;
time.tm_hour = hour;
time.tm_min = min;
time.tm_sec = sec;
wchar_t buf[64] = {};
UNSAFE_TODO(FXSYS_wcsftime(buf, 64, cFormat.c_str(), &time));
cFormat = buf;
return CJS_Result::Success(pRuntime->NewString(cFormat.c_str()));
}
CJS_Result CJS_Util::printx(CJS_Runtime* pRuntime,
pdfium::span<v8::Local<v8::Value>> params) {
if (params.size() < 2) {
return CJS_Result::Failure(JSMessage::kParamError);
}
return CJS_Result::Success(pRuntime->NewString(
StringPrintx(pRuntime->ToWideStringReentrant(params[0]),
pRuntime->ToWideStringReentrant(params[1]))
.AsStringView()));
}
// static
WideString CJS_Util::StringPrintx(const WideString& wsFormat,
const WideString& wsSource) {
WideString wsResult;
wsResult.Reserve(wsFormat.GetLength());
size_t iSourceIdx = 0;
size_t iFormatIdx = 0;
CaseMode eCaseMode = kPreserveCase;
bool bEscaped = false;
while (iFormatIdx < wsFormat.GetLength()) {
if (bEscaped) {
bEscaped = false;
wsResult += wsFormat[iFormatIdx];
++iFormatIdx;
continue;
}
switch (wsFormat[iFormatIdx]) {
case '\\': {
bEscaped = true;
++iFormatIdx;
} break;
case '<': {
eCaseMode = kLowerCase;
++iFormatIdx;
} break;
case '>': {
eCaseMode = kUpperCase;
++iFormatIdx;
} break;
case '=': {
eCaseMode = kPreserveCase;
++iFormatIdx;
} break;
case '?': {
if (iSourceIdx < wsSource.GetLength()) {
wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
++iSourceIdx;
}
++iFormatIdx;
} break;
case 'X': {
if (iSourceIdx < wsSource.GetLength()) {
if (isascii(wsSource[iSourceIdx]) && isalnum(wsSource[iSourceIdx])) {
wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
++iFormatIdx;
}
++iSourceIdx;
} else {
++iFormatIdx;
}
} break;
case 'A': {
if (iSourceIdx < wsSource.GetLength()) {
if (isascii(wsSource[iSourceIdx]) && isalpha(wsSource[iSourceIdx])) {
wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
++iFormatIdx;
}
++iSourceIdx;
} else {
++iFormatIdx;
}
} break;
case '9': {
if (iSourceIdx < wsSource.GetLength()) {
if (FXSYS_IsDecimalDigit(wsSource[iSourceIdx])) {
wsResult += wsSource[iSourceIdx];
++iFormatIdx;
}
++iSourceIdx;
} else {
++iFormatIdx;
}
} break;
case '*': {
if (iSourceIdx < wsSource.GetLength()) {
wsResult += TranslateCase(wsSource[iSourceIdx], eCaseMode);
++iSourceIdx;
} else {
++iFormatIdx;
}
} break;
default: {
wsResult += wsFormat[iFormatIdx];
++iFormatIdx;
} break;
}
}
return wsResult;
}
CJS_Result CJS_Util::scand(CJS_Runtime* pRuntime,
pdfium::span<v8::Local<v8::Value>> params) {
if (params.size() < 2) {
return CJS_Result::Failure(JSMessage::kParamError);
}
WideString sFormat = pRuntime->ToWideStringReentrant(params[0]);
WideString sDate = pRuntime->ToWideStringReentrant(params[1]);
double dDate = FX_GetDateTime();
if (sDate.GetLength() > 0) {
dDate = CJS_PublicMethods::ParseDateUsingFormat(pRuntime->GetIsolate(),
sDate, sFormat, nullptr);
}
if (isnan(dDate)) {
return CJS_Result::Success(pRuntime->NewUndefined());
}
return CJS_Result::Success(pRuntime->NewDate(dDate));
}
CJS_Result CJS_Util::byteToChar(CJS_Runtime* pRuntime,
pdfium::span<v8::Local<v8::Value>> params) {
if (params.size() < 1) {
return CJS_Result::Failure(JSMessage::kParamError);
}
int arg = pRuntime->ToInt32Reentrant(params[0]);
if (arg < 0 || arg > 255) {
return CJS_Result::Failure(JSMessage::kValueError);
}
WideString wStr(static_cast<wchar_t>(arg));
return CJS_Result::Success(pRuntime->NewString(wStr.AsStringView()));
}
// static
CJS_Util::DataType CJS_Util::ParseDataType(WideString* sFormat) {
enum State { kBefore, kFlags, kWidth, kPrecision, kSpecifier, kAfter };
DataType result = DataType::kInvalid;
State state = kBefore;
size_t precision_digits = 0;
size_t i = 0;
while (i < sFormat->GetLength()) {
wchar_t c = (*sFormat)[i];
switch (state) {
case kBefore:
if (c == L'%') {
state = kFlags;
}
break;
case kFlags:
if (c == L'+' || c == L'-' || c == L'#' || c == L' ') {
// Stay in same state.
} else {
state = kWidth;
continue; // Re-process same character.
}
break;
case kWidth:
if (c == L'*') {
return DataType::kInvalid;
}
if (FXSYS_IsDecimalDigit(c)) {
// Stay in same state.
} else if (c == L'.') {
state = kPrecision;
} else {
state = kSpecifier;
continue; // Re-process same character.
}
break;
case kPrecision:
if (c == L'*') {
return DataType::kInvalid;
}
if (FXSYS_IsDecimalDigit(c)) {
// Stay in same state.
++precision_digits;
} else {
state = kSpecifier;
continue; // Re-process same character.
}
break;
case kSpecifier:
if (c == L'c' || c == L'C' || c == L'd' || c == L'i' || c == L'o' ||
c == L'u' || c == L'x' || c == L'X') {
result = DataType::kInt;
} else if (c == L'e' || c == L'E' || c == L'f' || c == L'g' ||
c == L'G') {
result = DataType::kDouble;
} else if (c == L's' || c == L'S') {
// Map s to S since we always deal internally with wchar_t strings.
// TODO(tsepez): Probably 100% borked. %S is not a standard
// conversion.
sFormat->SetAt(i, L'S');
result = DataType::kString;
} else {
return DataType::kInvalid;
}
state = kAfter;
break;
case kAfter:
if (c == L'%') {
return DataType::kInvalid;
}
// Stay in same state until string exhausted.
break;
}
++i;
}
// See https://crbug.com/740166
if (result == DataType::kInt && precision_digits > 2) {
return DataType::kInvalid;
}
return result;
}