Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- Type1 fonts avoid signed overflow when they decrypt, keep binary ciphertext
bytes, and support unencrypted charstrings.

- CFF fonts bound INDEX, dictionary and glyph reads, and check offsets before
conversion. Real operands ignore the host locale.

Expand Down
4 changes: 2 additions & 2 deletions src/odr/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
#include <odr/internal/common/sheet_dependencies.hpp>
#include <odr/internal/common/sheet_recalculation.hpp>
#include <odr/internal/util/file_util.hpp>
#include <odr/internal/util/string_util.hpp>

#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <cstdlib>
#include <fstream>
Expand Down Expand Up @@ -152,7 +152,7 @@ CellValue parse_cell_value(const nlohmann::json &json) {
/// `#rrggbb`, as the page spells one.
Color parse_color(const std::string &text) {
const auto is_hex = [](const char c) {
return std::isxdigit(static_cast<unsigned char>(c)) != 0;
return internal::util::string::hex_digit(c).has_value();
};
if (text.size() != 7 || text[0] != '#' ||
!std::ranges::all_of(text.substr(1), is_hex)) {
Expand Down
29 changes: 12 additions & 17 deletions src/odr/internal/font/type1_crypt.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include <odr/internal/font/type1_crypt.hpp>

#include <cctype>
#include <odr/internal/util/string_util.hpp>

#include <cstdint>
#include <optional>
#include <string>

namespace odr::internal::font::type1 {
Expand All @@ -11,30 +13,23 @@ namespace {
constexpr std::uint16_t c1 = 52845;
constexpr std::uint16_t c2 = 22719;

[[nodiscard]] bool is_hex_digit(const std::uint8_t c) {
return std::isxdigit(c) != 0;
}

/// Hex-decode @p in, skipping whitespace; stops at the first non-hex, non-space
/// byte (the binary `eexec` form never reaches here).
[[nodiscard]] std::string hex_decode(const std::string_view in) {
std::string out;
std::int32_t high = -1;
for (const char ch : in) {
const auto c = static_cast<std::uint8_t>(ch);
if (std::isspace(c) != 0) {
if (util::string::is_ascii_whitespace(ch)) {
continue;
}
if (!is_hex_digit(c)) {
const std::optional<std::uint8_t> value = util::string::hex_digit(ch);
if (!value) {
break;
}
const std::int32_t value = (c <= '9') ? c - '0'
: (c <= 'F') ? c - 'A' + 10
: c - 'a' + 10;
if (high < 0) {
high = value;
high = *value;
} else {
out += static_cast<char>((high << 4) | value);
out += static_cast<char>((high << 4) | *value);
high = -1;
}
}
Expand All @@ -46,11 +41,10 @@ constexpr std::uint16_t c2 = 22719;
[[nodiscard]] bool looks_like_hex(const std::string_view eexec) {
std::int32_t seen = 0;
for (const char ch : eexec) {
const auto c = static_cast<std::uint8_t>(ch);
if (std::isspace(c) != 0) {
if (util::string::is_ascii_whitespace(ch)) {
continue;
}
if (!is_hex_digit(c)) {
if (!util::string::hex_digit(ch)) {
return false;
}
if (++seen == 4) {
Expand All @@ -74,8 +68,9 @@ std::string type1::decrypt(const std::string_view cipher,
for (const char ch : cipher) {
const auto c = static_cast<std::uint8_t>(ch);
out += static_cast<char>(c ^ (r >> 8));
r = static_cast<std::uint16_t>((c + r) * c1 + c2);
r = static_cast<std::uint16_t>((std::uint32_t{c} + r) * c1 + c2);
}
// A cipher shorter than its prefix decrypts to nothing.
if (skip >= out.size()) {
return {};
}
Expand Down
13 changes: 1 addition & 12 deletions src/odr/internal/font/type1_crypt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,12 @@

namespace odr::internal::font::type1 {

// Type1 (Adobe Type 1 Font Format) `eexec` / charstring decryption.
//
// Both the `eexec`-encrypted portion of the font program and each individual
// charstring use the same stream cipher with different keys: a 16-bit running
// key `R`, constants c1 = 52845 / c2 = 22719, where each plaintext byte is
// `cipher ^ (R >> 8)` and `R = (cipher + R) * c1 + c2` (mod 2^16). The leading
// @p skip bytes of plaintext are random padding and discarded.

/// Decrypt @p cipher with the running-key cipher seeded at @p key, discarding
/// the first @p skip plaintext bytes.
[[nodiscard]] std::string decrypt(std::string_view cipher, std::uint16_t key,
std::size_t skip);

/// Decrypt the `eexec` section (key 55665, 4 random bytes). Accepts either the
/// binary form (PDF `/FontFile`, the `/Length2` portion) or the ASCII-hex form
/// (PFA fonts): when the section's leading bytes are all hex digits/whitespace
/// it is hex-decoded first.
/// Decrypt binary or ASCII-hex eexec data (Adobe Type 1 Font Format 7.2).
[[nodiscard]] std::string decrypt_eexec(std::string_view eexec);

/// Decrypt one charstring (key 4330), discarding @p len_iv leading bytes
Expand Down
66 changes: 40 additions & 26 deletions src/odr/internal/font/type1_font.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include <odr/internal/font/type1_font.hpp>

#include <odr/internal/font/type1_crypt.hpp>
#include <odr/internal/pdf/pdf_object_parser.hpp>
#include <odr/internal/util/byte_util.hpp>
#include <odr/internal/util/number_util.hpp>

#include <algorithm>
#include <charconv>
#include <cmath>
#include <cstdint>
#include <optional>
#include <stdexcept>
Expand All @@ -16,14 +19,15 @@ namespace odr::internal::font::type1 {

namespace {

[[nodiscard]] bool is_ps_space(const char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' ||
c == '\0';
/// The white space after `eexec`. NUL and form feed can start the binary
/// ciphertext, so they are not skipped here.
[[nodiscard]] bool is_eexec_space(const char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}

/// Skip PostScript whitespace starting at @p p.
[[nodiscard]] std::size_t skip_space(const std::string_view s, std::size_t p) {
while (p < s.size() && is_ps_space(s[p])) {
while (p < s.size() && pdf::ObjectParser::is_whitespace(s[p])) {
++p;
}
return p;
Expand All @@ -34,7 +38,7 @@ namespace {
std::size_t &p) {
p = skip_space(s, p);
const std::size_t begin = p;
while (p < s.size() && !is_ps_space(s[p])) {
while (p < s.size() && !pdf::ObjectParser::is_whitespace(s[p])) {
++p;
}
return s.substr(begin, p - begin);
Expand All @@ -48,13 +52,9 @@ namespace {
}

[[nodiscard]] double parse_double(const std::string_view token) {
// std::from_chars for double is not universally available; std::stod needs a
// null-terminated copy.
try {
return std::stod(std::string(token));
} catch (const std::exception &) {
return 0.0;
}
// An unreadable number reads as zero, so it does not refuse the font.
const std::optional<double> value = util::number::parse(token);
return value && std::isfinite(*value) ? *value : 0.0;
}

/// A `/FontBBox` number as an FWord. Clamping keeps an out-of-range number out
Expand All @@ -71,7 +71,7 @@ parse_number_array(const std::string_view s, const std::string_view key) {
if (k == std::string_view::npos) {
return out;
}
std::size_t open = s.find_first_of("[{", k);
const std::size_t open = s.find_first_of("[{", k);
if (open == std::string_view::npos) {
return out;
}
Expand Down Expand Up @@ -111,7 +111,7 @@ read_rd_binary(const std::string_view s, std::size_t &p) {
return std::nullopt;
}
++q; // the single delimiter space
if (q + static_cast<std::size_t>(length) > s.size()) {
if (static_cast<std::size_t>(length) > s.size() - q) {
return std::nullopt;
}
const std::string_view bytes = s.substr(q, static_cast<std::size_t>(length));
Expand All @@ -135,16 +135,22 @@ Type1Font::Type1Font(std::string_view data) {
std::string unframed;
if (!data.empty() && static_cast<std::uint8_t>(data[0]) == 0x80) {
std::size_t p = 0;
while (p + 6 <= data.size() && static_cast<std::uint8_t>(data[p]) == 0x80) {
while (p < data.size()) {
if (data.size() - p < 2 || static_cast<std::uint8_t>(data[p]) != 0x80) {
throw std::runtime_error("type1: invalid PFB segment");
}
const std::uint8_t type = static_cast<std::uint8_t>(data[p + 1]);
if (type == 3) {
break;
}
if ((type != 1 && type != 2) || data.size() - p < 6) {
throw std::runtime_error("type1: invalid PFB segment header");
}
const auto len =
util::byte::from_little_endian<std::uint32_t>(data.substr(p + 2, 4));
p += 6;
if (p + len > data.size()) {
break;
if (len > data.size() - p) {
throw std::runtime_error("type1: truncated PFB segment");
}
unframed.append(data.substr(p, len));
p += len;
Expand All @@ -160,7 +166,10 @@ Type1Font::Type1Font(std::string_view data) {
parse_clear(data.substr(0, eexec));

// The encrypted blob begins after `eexec` and its trailing whitespace.
const std::size_t blob = skip_space(data, eexec + 5);
std::size_t blob = eexec + 5;
while (blob < data.size() && is_eexec_space(data[blob])) {
++blob;
}
const std::string decrypted = decrypt_eexec(data.substr(blob));
parse_private(decrypted);

Expand Down Expand Up @@ -218,18 +227,23 @@ void Type1Font::parse_clear(const std::string_view clear) {
}

void Type1Font::parse_private(const std::string_view decrypted) {
std::size_t len_iv = 4;
std::int32_t len_iv = 4;
if (const std::size_t k = decrypted.find("/lenIV");
k != std::string_view::npos) {
std::size_t p = k + 6;
std::int32_t value = 0;
if (parse_int(read_token(decrypted, p), value)) {
if (value < 0) {
throw std::runtime_error("type1: negative /lenIV");
if (value < -1) {
throw std::runtime_error("type1: invalid /lenIV");
}
len_iv = static_cast<std::size_t>(value);
len_iv = value;
}
}
const auto decode = [len_iv](const std::string_view bytes) {
return len_iv == -1
? std::string(bytes)
: decrypt_charstring(bytes, static_cast<std::size_t>(len_iv));
};

// /CharStrings starts where /Subrs ends (Subrs precede it).
const std::size_t cs = decrypted.find("/CharStrings");
Expand Down Expand Up @@ -257,10 +271,10 @@ void Type1Font::parse_private(const std::string_view decrypted) {
p += 4;
continue;
}
if (static_cast<std::int32_t>(m_subrs.size()) <= index) {
m_subrs.resize(index + 1);
if (m_subrs.size() <= static_cast<std::size_t>(index)) {
m_subrs.resize(static_cast<std::size_t>(index) + 1);
}
m_subrs[index] = decrypt_charstring(*bytes, len_iv);
m_subrs[index] = decode(*bytes);
p = q;
}
}
Expand All @@ -284,7 +298,7 @@ void Type1Font::parse_private(const std::string_view decrypted) {
p = slash + 1;
continue;
}
m_glyphs.push_back({std::move(name), decrypt_charstring(*bytes, len_iv)});
m_glyphs.push_back({std::move(name), decode(*bytes)});
p = q;
}
}
Expand Down
13 changes: 2 additions & 11 deletions src/odr/internal/font/type1_font.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,8 @@ struct Glyph {
std::string charstring;
};

/// @brief Parses an Adobe Type1 font program into its decrypted parts.
///
/// A Type1 program has three sections: a clear-text header (font dictionary up
/// to `eexec`), an `eexec`-encrypted private portion (`/Subrs`,
/// `/CharStrings`), and a zero-padded trailer. This reads `/FontMatrix`,
/// `/FontBBox`, `/Encoding` and `/FontName` from the header, decrypts the
/// `eexec` section (`type1_crypt`) and keeps every glyph's decrypted
/// charstring plus the `/Subrs`; interpreting them is `type1_charstring`.
///
/// Throws `std::runtime_error` when the program has no `eexec` section or no
/// `/CharStrings`.
/// Parse Type1 metadata and decrypt its charstrings and subroutines.
/// Throws on invalid numbers, framing, or missing eexec/CharStrings data.
class Type1Font {
public:
/// Cheap magic test: the PostScript font sentinel (`%!PS-AdobeFont`,
Expand Down
14 changes: 5 additions & 9 deletions src/odr/internal/pdf/pdf_object_parser.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include <odr/internal/pdf/pdf_object_parser.hpp>

#include <odr/internal/util/stream_util.hpp>
#include <odr/internal/util/string_util.hpp>

#include <algorithm>
#include <cctype>
Expand Down Expand Up @@ -86,16 +87,11 @@ std::uint8_t ObjectParser::octet_char_to_int(const char_type c) {
}

std::uint8_t ObjectParser::hex_char_to_int(const char_type c) {
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
const std::optional<std::uint8_t> value = util::string::hex_digit(c);
if (!value) {
throw std::runtime_error("invalid character in hex_char_to_int");
}
throw std::runtime_error("invalid character in hex_char_to_int");
return *value;
}

ObjectParser::char_type ObjectParser::two_hex_to_char(const char_type first,
Expand Down
1 change: 1 addition & 0 deletions src/odr/internal/pdf/pdf_object_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class ObjectParser {
static char_type three_octet_to_char(char_type first, char_type second,
char_type third);

/// PostScript white space, which PDF shares (7.2.2).
static bool is_whitespace(char c);
/// The delimiters of 7.2.2, each of which opens a token of its own.
static bool is_delimiter(char c);
Expand Down
14 changes: 5 additions & 9 deletions src/odr/internal/rtf/rtf_tokenizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <algorithm>
#include <cstdint>
#include <limits>
#include <optional>
#include <stdexcept>
#include <utility>

Expand All @@ -16,16 +17,11 @@ namespace str = util::string;
namespace {

std::uint8_t hex_char_to_int(const char c) {
if (c >= '0' && c <= '9') {
return static_cast<std::uint8_t>(c - '0');
const std::optional<std::uint8_t> value = str::hex_digit(c);
if (!value) {
throw std::runtime_error("invalid hex digit");
}
if (c >= 'a' && c <= 'f') {
return static_cast<std::uint8_t>(c - 'a' + 10);
}
if (c >= 'A' && c <= 'F') {
return static_cast<std::uint8_t>(c - 'A' + 10);
}
throw std::runtime_error("invalid hex digit");
return *value;
}

char two_hex_to_char(const char first, const char second) {
Expand Down
Loading
Loading