aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJay Berkenbilt <ejb@ql.org>2024-02-04 21:59:18 +0100
committerJay Berkenbilt <ejb@ql.org>2024-02-04 23:27:49 +0100
commitcb0f390cc1f98a8e82b27259f8f3cd5f162992eb (patch)
treef60552d33dac33d5e146947ef97e988b91071d46
parent7caa9ddf5a8b272c94fa5d4c079f2be8eabff983 (diff)
downloadqpdf-cb0f390cc1f98a8e82b27259f8f3cd5f162992eb.tar.zst
Handle parse error stream data (fixes #1123)
A parse error in stream data in which stream data contained a nested object would cause a crash because qpdf was not correctly updating its internal state. Rework the QPDF json reactor to not be sensitive to parse errors in this way.
-rw-r--r--libqpdf/QPDF_json.cc287
-rw-r--r--qpdf/qpdf.testcov4
-rw-r--r--qpdf/qtest/qpdf-json.test2
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-data2.json71
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-data2.out2
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-datafile2.json71
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-datafile2.out2
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version1.out4
-rw-r--r--qpdf/qtest/qpdf/qjson-bad-pdf-version2.out4
-rw-r--r--qpdf/qtest/qpdf/qjson-obj-key-errors.out6
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out3
-rw-r--r--qpdf/qtest/qpdf/qjson-stream-not-dict.out1
-rw-r--r--qpdf/qtest/qpdf/qjson-trailer-stream.out1
-rw-r--r--qpdf/qtest/qpdf/update-from-json-errors.out4
14 files changed, 311 insertions, 151 deletions
diff --git a/libqpdf/QPDF_json.cc b/libqpdf/QPDF_json.cc
index 33211f14..502f2346 100644
--- a/libqpdf/QPDF_json.cc
+++ b/libqpdf/QPDF_json.cc
@@ -14,7 +14,7 @@
// This chart shows an example of the state transitions that would occur in parsing a minimal file.
-// | st_initial
+// |
// { | -> st_top
// "qpdf": [ | -> st_qpdf
// { | -> st_qpdf_meta
@@ -47,7 +47,7 @@
// } | <- st_objects
// } | <- st_qpdf
// ] | <- st_top
-// } | <- st_initial
+// } |
static char const* JSON_PDF = (
// force line break
@@ -99,7 +99,7 @@ is_indirect_object(std::string const& v, int& obj, int& gen)
}
obj = QUtil::string_to_int(o_str.c_str());
gen = QUtil::string_to_int(g_str.c_str());
- return true;
+ return obj > 0;
}
static bool
@@ -256,7 +256,6 @@ class QPDF::JSONReactor: public JSON::Reactor
private:
enum state_e {
- st_initial,
st_top,
st_qpdf,
st_qpdf_meta,
@@ -268,28 +267,35 @@ class QPDF::JSONReactor: public JSON::Reactor
st_ignore,
};
+ struct StackFrame
+ {
+ StackFrame(state_e state) :
+ state(state){};
+ StackFrame(state_e state, QPDFObjectHandle&& object) :
+ state(state),
+ object(object){};
+ state_e state;
+ QPDFObjectHandle object;
+ };
+
void containerStart();
- void nestedState(std::string const& key, JSON const& value, state_e);
+ bool setNextStateIfDictionary(std::string const& key, JSON const& value, state_e);
void setObjectDescription(QPDFObjectHandle& oh, JSON const& value);
QPDFObjectHandle makeObject(JSON const& value);
void error(qpdf_offset_t offset, std::string const& message);
- void
- replaceObject(QPDFObjectHandle to_replace, QPDFObjectHandle replacement, JSON const& value);
+ void replaceObject(QPDFObjectHandle&& replacement, JSON const& value);
QPDF& pdf;
std::shared_ptr<InputSource> is;
bool must_be_complete{true};
std::shared_ptr<QPDFValue::Description> descr;
bool errors{false};
- bool parse_error{false};
bool saw_qpdf{false};
bool saw_qpdf_meta{false};
bool saw_objects{false};
bool saw_json_version{false};
bool saw_pdf_version{false};
bool saw_trailer{false};
- state_e state{st_initial};
- state_e next_state{st_top};
std::string cur_object;
bool saw_value{false};
bool saw_stream{false};
@@ -297,9 +303,10 @@ class QPDF::JSONReactor: public JSON::Reactor
bool saw_data{false};
bool saw_datafile{false};
bool this_stream_needs_data{false};
- std::vector<state_e> state_stack{st_initial};
- std::vector<QPDFObjectHandle> object_stack;
std::set<QPDFObjGen> reserved;
+ std::vector<StackFrame> stack;
+ QPDFObjectHandle next_obj;
+ state_e next_state{st_top};
};
void
@@ -322,8 +329,12 @@ QPDF::JSONReactor::anyErrors() const
void
QPDF::JSONReactor::containerStart()
{
- state_stack.push_back(state);
- state = next_state;
+ if (next_obj.isInitialized()) {
+ stack.emplace_back(next_state, std::move(next_obj));
+ next_obj = QPDFObjectHandle();
+ } else {
+ stack.emplace_back(next_state);
+ }
}
void
@@ -335,20 +346,19 @@ QPDF::JSONReactor::dictionaryStart()
void
QPDF::JSONReactor::arrayStart()
{
- containerStart();
- if (state == st_top) {
+ if (stack.empty()) {
QTC::TC("qpdf", "QPDF_json top-level array");
throw std::runtime_error("QPDF JSON must be a dictionary");
}
+ containerStart();
}
void
QPDF::JSONReactor::containerEnd(JSON const& value)
{
- auto from_state = state;
- state = state_stack.back();
- state_stack.pop_back();
- if (state == st_initial) {
+ auto from_state = stack.back().state;
+ stack.pop_back();
+ if (stack.empty()) {
if (!this->saw_qpdf) {
QTC::TC("qpdf", "QPDF_json missing qpdf");
error(0, "\"qpdf\" object was not seen");
@@ -371,26 +381,16 @@ QPDF::JSONReactor::containerEnd(JSON const& value)
}
}
}
- } else if (state == st_objects) {
- if (parse_error) {
- QTC::TC("qpdf", "QPDF_json don't check object after parse error");
- } else if (cur_object == "trailer") {
- if (!saw_value) {
- QTC::TC("qpdf", "QPDF_json trailer no value");
- error(value.getStart(), "\"trailer\" is missing \"value\"");
- }
- } else if (saw_value == saw_stream) {
+ } else if (from_state == st_trailer) {
+ if (!saw_value) {
+ QTC::TC("qpdf", "QPDF_json trailer no value");
+ error(value.getStart(), "\"trailer\" is missing \"value\"");
+ }
+ } else if (from_state == st_object_top) {
+ if (saw_value == saw_stream) {
QTC::TC("qpdf", "QPDF_json value stream both or neither");
error(value.getStart(), "object must have exactly one of \"value\" or \"stream\"");
}
- object_stack.clear();
- this->cur_object = "";
- this->saw_dict = false;
- this->saw_data = false;
- this->saw_datafile = false;
- this->saw_value = false;
- this->saw_stream = false;
- } else if (state == st_object_top) {
if (saw_stream) {
if (!saw_dict) {
QTC::TC("qpdf", "QPDF_json stream no dict");
@@ -414,11 +414,7 @@ QPDF::JSONReactor::containerEnd(JSON const& value)
}
}
}
- } else if ((state == st_stream) || (state == st_object)) {
- if (!parse_error) {
- object_stack.pop_back();
- }
- } else if ((state == st_top) && (from_state == st_qpdf)) {
+ } else if (from_state == st_qpdf) {
// Handle dangling indirect object references which the PDF spec says to treat as nulls.
// It's tempting to make this an error, but that would be wrong since valid input files may
// have these.
@@ -429,16 +425,27 @@ QPDF::JSONReactor::containerEnd(JSON const& value)
}
}
}
+ if (!stack.empty()) {
+ auto state = stack.back().state;
+ if (state == st_objects) {
+ this->cur_object = "";
+ this->saw_dict = false;
+ this->saw_data = false;
+ this->saw_datafile = false;
+ this->saw_value = false;
+ this->saw_stream = false;
+ }
+ }
}
void
-QPDF::JSONReactor::replaceObject(
- QPDFObjectHandle to_replace, QPDFObjectHandle replacement, JSON const& value)
+QPDF::JSONReactor::replaceObject(QPDFObjectHandle&& replacement, JSON const& value)
{
- auto og = to_replace.getObjGen();
+ auto& tos = stack.back();
+ auto og = tos.object.getObjGen();
this->pdf.replaceObject(og, replacement);
- auto oh = pdf.getObject(og);
- setObjectDescription(oh, value);
+ next_obj = pdf.getObject(og);
+ setObjectDescription(tos.object, value);
}
void
@@ -448,22 +455,26 @@ QPDF::JSONReactor::topLevelScalar()
throw std::runtime_error("QPDF JSON must be a dictionary");
}
-void
-QPDF::JSONReactor::nestedState(std::string const& key, JSON const& value, state_e next)
+bool
+QPDF::JSONReactor::setNextStateIfDictionary(std::string const& key, JSON const& value, state_e next)
{
// Use this method when the next state is for processing a nested dictionary.
if (value.isDictionary()) {
this->next_state = next;
- } else {
- error(value.getStart(), "\"" + key + "\" must be a dictionary");
- this->next_state = st_ignore;
- this->parse_error = true;
+ return true;
}
+ error(value.getStart(), "\"" + key + "\" must be a dictionary");
+ return false;
}
bool
QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
{
+ if (stack.empty()) {
+ throw std::logic_error("stack is empty in dictionaryItem");
+ }
+ next_state = st_ignore;
+ auto state = stack.back().state;
if (state == st_ignore) {
QTC::TC("qpdf", "QPDF_json ignoring in st_ignore");
// ignore
@@ -473,51 +484,48 @@ QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
if (!value.isArray()) {
QTC::TC("qpdf", "QPDF_json qpdf not array");
error(value.getStart(), "\"qpdf\" must be an array");
- next_state = st_ignore;
- parse_error = true;
} else {
next_state = st_qpdf;
}
} else {
// Ignore all other fields.
QTC::TC("qpdf", "QPDF_json ignoring unknown top-level key");
- next_state = st_ignore;
}
} else if (state == st_qpdf_meta) {
if (key == "pdfversion") {
this->saw_pdf_version = true;
- bool version_okay = false;
std::string v;
+ bool okay = false;
if (value.getString(v)) {
std::string version;
char const* p = v.c_str();
if (QPDF::validatePDFVersion(p, version) && (*p == '\0')) {
- version_okay = true;
this->pdf.m->pdf_version = version;
+ okay = true;
}
}
- if (!version_okay) {
+ if (!okay) {
QTC::TC("qpdf", "QPDF_json bad pdf version");
- error(value.getStart(), "invalid PDF version (must be x.y)");
+ error(value.getStart(), "invalid PDF version (must be \"x.y\")");
}
} else if (key == "jsonversion") {
this->saw_json_version = true;
- bool version_okay = false;
std::string v;
+ bool okay = false;
if (value.getNumber(v)) {
std::string version;
if (QUtil::string_to_int(v.c_str()) == 2) {
- version_okay = true;
+ okay = true;
}
}
- if (!version_okay) {
+ if (!okay) {
QTC::TC("qpdf", "QPDF_json bad json version");
- error(value.getStart(), "invalid JSON version (must be 2)");
+ error(value.getStart(), "invalid JSON version (must be numeric value 2)");
}
} else if (key == "pushedinheritedpageresources") {
bool v;
if (value.getBool(v)) {
- if ((!this->must_be_complete) && v) {
+ if (!this->must_be_complete && v) {
this->pdf.pushInheritedAttributesToPage();
}
} else {
@@ -527,7 +535,7 @@ QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
} else if (key == "calledgetallpages") {
bool v;
if (value.getBool(v)) {
- if ((!this->must_be_complete) && v) {
+ if (!this->must_be_complete && v) {
this->pdf.getAllPages();
}
} else {
@@ -538,103 +546,95 @@ QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
// ignore unknown keys for forward compatibility and to skip keys we don't care about
// like "maxobjectid".
QTC::TC("qpdf", "QPDF_json ignore second-level key");
- next_state = st_ignore;
}
} else if (state == st_objects) {
int obj = 0;
int gen = 0;
if (key == "trailer") {
this->saw_trailer = true;
- nestedState(key, value, st_trailer);
this->cur_object = "trailer";
+ setNextStateIfDictionary(key, value, st_trailer);
} else if (is_obj_key(key, obj, gen)) {
this->cur_object = key;
- auto oh = pdf.reserveObjectIfNotExists(QPDFObjGen(obj, gen));
- object_stack.push_back(oh);
- nestedState(key, value, st_object_top);
+ if (setNextStateIfDictionary(key, value, st_object_top)) {
+ next_obj = pdf.reserveObjectIfNotExists(QPDFObjGen(obj, gen));
+ }
} else {
QTC::TC("qpdf", "QPDF_json bad object key");
error(value.getStart(), "object key should be \"trailer\" or \"obj:n n R\"");
- next_state = st_ignore;
- parse_error = true;
}
} else if (state == st_object_top) {
- if (object_stack.size() == 0) {
- throw std::logic_error("no object on stack in st_object_top");
+ if (stack.empty()) {
+ throw std::logic_error("stack empty in st_object_top");
+ }
+ auto& tos = stack.back();
+ if (!tos.object.isInitialized()) {
+ throw std::logic_error("current object uninitialized in st_object_top");
}
- auto tos = object_stack.back();
- QPDFObjectHandle replacement;
if (key == "value") {
- // Don't use nestedState since this can have any type.
+ // Don't use setNextStateIfDictionary since this can have any type.
this->saw_value = true;
+ replaceObject(makeObject(value), value);
next_state = st_object;
- replacement = makeObject(value);
- replaceObject(tos, replacement, value);
} else if (key == "stream") {
this->saw_stream = true;
- nestedState(key, value, st_stream);
- this->this_stream_needs_data = false;
- if (tos.isStream()) {
- QTC::TC("qpdf", "QPDF_json updating existing stream");
+ if (setNextStateIfDictionary(key, value, st_stream)) {
+ this->this_stream_needs_data = false;
+ if (tos.object.isStream()) {
+ QTC::TC("qpdf", "QPDF_json updating existing stream");
+ } else {
+ this->this_stream_needs_data = true;
+ replaceObject(pdf.reserveStream(tos.object.getObjGen()), value);
+ }
+ next_obj = tos.object;
} else {
- this->this_stream_needs_data = true;
- replacement = pdf.reserveStream(tos.getObjGen());
- replaceObject(tos, replacement, value);
+ // Error message already given above
+ QTC::TC("qpdf", "QPDF_json stream not a dictionary");
}
} else {
// Ignore unknown keys for forward compatibility
QTC::TC("qpdf", "QPDF_json ignore unknown key in object_top");
- next_state = st_ignore;
- }
- if (replacement.isInitialized()) {
- object_stack.pop_back();
- object_stack.push_back(replacement);
}
} else if (state == st_trailer) {
if (key == "value") {
this->saw_value = true;
- // The trailer must be a dictionary, so we can use nestedState.
- nestedState("trailer.value", value, st_object);
- this->pdf.m->trailer = makeObject(value);
- setObjectDescription(this->pdf.m->trailer, value);
+ // The trailer must be a dictionary, so we can use setNextStateIfDictionary.
+ if (setNextStateIfDictionary("trailer.value", value, st_object)) {
+ this->pdf.m->trailer = makeObject(value);
+ setObjectDescription(this->pdf.m->trailer, value);
+ }
} else if (key == "stream") {
// Don't need to set saw_stream here since there's already an error.
QTC::TC("qpdf", "QPDF_json trailer stream");
error(value.getStart(), "the trailer may not be a stream");
- next_state = st_ignore;
- parse_error = true;
} else {
// Ignore unknown keys for forward compatibility
QTC::TC("qpdf", "QPDF_json ignore unknown key in trailer");
- next_state = st_ignore;
}
} else if (state == st_stream) {
- if (object_stack.size() == 0) {
- throw std::logic_error("no object on stack in st_stream");
+ if (stack.empty()) {
+ throw std::logic_error("stack empty in st_stream");
}
- auto tos = object_stack.back();
- if (!tos.isStream()) {
- throw std::logic_error("top of stack is not stream in st_stream");
+ auto& tos = stack.back();
+ if (!tos.object.isStream()) {
+ throw std::logic_error("current object is not stream in st_stream");
}
auto uninitialized = QPDFObjectHandle();
if (key == "dict") {
this->saw_dict = true;
- // Since a stream dictionary must be a dictionary, we can use nestedState to transition
- // to st_value.
- nestedState("stream.dict", value, st_object);
- auto dict = makeObject(value);
- if (dict.isDictionary()) {
- tos.replaceDict(dict);
+ if (setNextStateIfDictionary("stream.dict", value, st_object)) {
+ tos.object.replaceDict(makeObject(value));
} else {
- // An error had already been given by nestedState
+ // An error had already been given by setNextStateIfDictionary
QTC::TC("qpdf", "QPDF_json stream dict not dict");
- parse_error = true;
}
} else if (key == "data") {
this->saw_data = true;
std::string v;
if (!value.getString(v)) {
+ QTC::TC("qpdf", "QPDF_json stream data not string");
error(value.getStart(), "\"stream.data\" must be a string");
+ tos.object.replaceStreamData("", uninitialized, uninitialized);
} else {
// The range includes the quotes.
auto start = value.getStart() + 1;
@@ -642,34 +642,42 @@ QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
if (end < start) {
throw std::logic_error("QPDF_json: JSON string length < 0");
}
- tos.replaceStreamData(provide_data(is, start, end), uninitialized, uninitialized);
+ tos.object.replaceStreamData(
+ provide_data(is, start, end), uninitialized, uninitialized);
}
} else if (key == "datafile") {
this->saw_datafile = true;
std::string filename;
- if (value.getString(filename)) {
- tos.replaceStreamData(QUtil::file_provider(filename), uninitialized, uninitialized);
- } else {
+ if (!value.getString(filename)) {
+ QTC::TC("qpdf", "QPDF_json stream datafile not string");
error(
value.getStart(),
- "\"stream.datafile\" must be a string containing a file "
- "name");
+ "\"stream.datafile\" must be a string containing a file name");
+ tos.object.replaceStreamData("", uninitialized, uninitialized);
+ } else {
+ tos.object.replaceStreamData(
+ QUtil::file_provider(filename), uninitialized, uninitialized);
}
} else {
// Ignore unknown keys for forward compatibility.
QTC::TC("qpdf", "QPDF_json ignore unknown key in stream");
- next_state = st_ignore;
}
} else if (state == st_object) {
- if (!parse_error) {
- auto dict = object_stack.back();
- if (dict.isStream()) {
- dict = dict.getDict();
- }
- dict.replaceKey(
- is_pdf_name(key) ? QPDFObjectHandle::parse(key.substr(2)).getName() : key,
- makeObject(value));
+ if (stack.empty()) {
+ throw std::logic_error("stack empty in st_object");
+ }
+ auto& tos = stack.back();
+ auto dict = tos.object;
+ if (dict.isStream()) {
+ dict = dict.getDict();
+ }
+ if (!dict.isDictionary()) {
+ throw std::logic_error(
+ "current object is not stream or dictionary in st_object dictionary item");
}
+ dict.replaceKey(
+ is_pdf_name(key) ? QPDFObjectHandle::parse(key.substr(2)).getName() : key,
+ makeObject(value));
} else {
throw std::logic_error("QPDF_json: unknown state " + std::to_string(state));
}
@@ -679,25 +687,24 @@ QPDF::JSONReactor::dictionaryItem(std::string const& key, JSON const& value)
bool
QPDF::JSONReactor::arrayItem(JSON const& value)
{
+ if (stack.empty()) {
+ throw std::logic_error("stack is empty in arrayItem");
+ }
+ next_state = st_ignore;
+ auto state = stack.back().state;
if (state == st_qpdf) {
if (!this->saw_qpdf_meta) {
this->saw_qpdf_meta = true;
- nestedState("qpdf[0]", value, st_qpdf_meta);
+ setNextStateIfDictionary("qpdf[0]", value, st_qpdf_meta);
} else if (!this->saw_objects) {
this->saw_objects = true;
- nestedState("qpdf[1]", value, st_objects);
+ setNextStateIfDictionary("qpdf[1]", value, st_objects);
} else {
QTC::TC("qpdf", "QPDF_json more than two qpdf elements");
error(value.getStart(), "\"qpdf\" must have two elements");
- next_state = st_ignore;
- parse_error = true;
- }
- }
- if (state == st_object) {
- if (!parse_error) {
- auto tos = object_stack.back();
- tos.appendItem(makeObject(value));
}
+ } else if (state == st_object) {
+ stack.back().object.appendItem(makeObject(value));
}
return true;
}
@@ -722,10 +729,12 @@ QPDF::JSONReactor::makeObject(JSON const& value)
bool bool_v = false;
if (value.isDictionary()) {
result = QPDFObjectHandle::newDictionary();
- object_stack.push_back(result);
+ next_obj = result;
+ next_state = st_object;
} else if (value.isArray()) {
result = QPDFObjectHandle::newArray();
- object_stack.push_back(result);
+ next_obj = result;
+ next_state = st_object;
} else if (value.isNull()) {
result = QPDFObjectHandle::newNull();
} else if (value.getBool(bool_v)) {
diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov
index 51c3ea72..7501a4b5 100644
--- a/qpdf/qpdf.testcov
+++ b/qpdf/qpdf.testcov
@@ -668,7 +668,6 @@ QPDF_json value stream both or neither 0
QPDFJob need json-stream-prefix for stdout 0
QPDFJob write json to stdout 0
QPDFJob write json to file 0
-QPDF_json don't check object after parse error 0
QPDF_json ignoring unknown top-level key 0
QPDF_json ignore second-level key 0
QPDF_json ignore unknown key in object_top 0
@@ -694,3 +693,6 @@ QPDFJob misplaced page range 0
QPDFJob duplicated range 0
QPDFJob json over/under no file 0
QPDF_Array copy 1
+QPDF_json stream data not string 0
+QPDF_json stream datafile not string 0
+QPDF_json stream not a dictionary 0
diff --git a/qpdf/qtest/qpdf-json.test b/qpdf/qtest/qpdf-json.test
index 0ea126ec..2f7bcd86 100644
--- a/qpdf/qtest/qpdf-json.test
+++ b/qpdf/qtest/qpdf-json.test
@@ -37,6 +37,8 @@ my @badfiles = (
'obj-key-errors',
'bad-data',
'bad-datafile',
+ 'bad-data2',
+ 'bad-datafile2',
);
$n_tests += scalar(@badfiles);
diff --git a/qpdf/qtest/qpdf/qjson-bad-data2.json b/qpdf/qtest/qpdf/qjson-bad-data2.json
new file mode 100644
index 00000000..80206086
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-data2.json
@@ -0,0 +1,71 @@
+{
+ "qpdf": [
+ {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6
+ },
+ {
+ "obj:1 0 R": {
+ "value": {
+ "/Pages": "2 0 R",
+ "/Type": "/Catalog"
+ }
+ },
+ "obj:2 0 R": {
+ "value": {
+ "/Count": 1,
+ "/Kids": [
+ "3 0 R"
+ ],
+ "/Type": "/Pages"
+ }
+ },
+ "obj:3 0 R": {
+ "value": {
+ "/Contents": ["4 0 R", "7 0 R"],
+ "/MediaBox": [
+ 0,
+ 0,
+ 612,
+ 792
+ ],
+ "/Parent": "2 0 R",
+ "/Resources": {
+ "/Font": {
+ "/F1": "6 0 R"
+ },
+ "/ProcSet": "5 0 R"
+ },
+ "/Type": "/Page"
+ }
+ },
+ "obj:4 0 R": {
+ "stream": {
+ "data": [[]],
+ "dict": {}
+ }
+ },
+ "obj:5 0 R": {
+ "value": [
+ "/PDF",
+ "/Text"
+ ]
+ },
+ "obj:6 0 R": {
+ "value": {
+ "/BaseFont": "/Helvetica",
+ "/Encoding": "/WinAnsiEncoding",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ ]
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-data2.out b/qpdf/qtest/qpdf/qjson-bad-data2.out
new file mode 100644
index 00000000..47c83c8e
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-data2.out
@@ -0,0 +1,2 @@
+WARNING: qjson-bad-data2.json (obj:4 0 R, offset 846): "stream.data" must be a string
+qpdf: qjson-bad-data2.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-bad-datafile2.json b/qpdf/qtest/qpdf/qjson-bad-datafile2.json
new file mode 100644
index 00000000..b5c99820
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-datafile2.json
@@ -0,0 +1,71 @@
+{
+ "qpdf": [
+ {
+ "jsonversion": 2,
+ "pdfversion": "1.3",
+ "maxobjectid": 6
+ },
+ {
+ "obj:1 0 R": {
+ "value": {
+ "/Pages": "2 0 R",
+ "/Type": "/Catalog"
+ }
+ },
+ "obj:2 0 R": {
+ "value": {
+ "/Count": 1,
+ "/Kids": [
+ "3 0 R"
+ ],
+ "/Type": "/Pages"
+ }
+ },
+ "obj:3 0 R": {
+ "value": {
+ "/Contents": ["4 0 R", "7 0 R"],
+ "/MediaBox": [
+ 0,
+ 0,
+ 612,
+ 792
+ ],
+ "/Parent": "2 0 R",
+ "/Resources": {
+ "/Font": {
+ "/F1": "6 0 R"
+ },
+ "/ProcSet": "5 0 R"
+ },
+ "/Type": "/Page"
+ }
+ },
+ "obj:4 0 R": {
+ "stream": {
+ "datafile": [[]],
+ "dict": {}
+ }
+ },
+ "obj:5 0 R": {
+ "value": [
+ "/PDF",
+ "/Text"
+ ]
+ },
+ "obj:6 0 R": {
+ "value": {
+ "/BaseFont": "/Helvetica",
+ "/Encoding": "/WinAnsiEncoding",
+ "/Subtype": "/Type1",
+ "/Type": "/Font"
+ }
+ },
+ "trailer": {
+ "value": {
+ "/Root": "1 0 R",
+ "/Size": 7
+ }
+ }
+ }
+ ]
+}
diff --git a/qpdf/qtest/qpdf/qjson-bad-datafile2.out b/qpdf/qtest/qpdf/qjson-bad-datafile2.out
new file mode 100644
index 00000000..41949a21
--- /dev/null
+++ b/qpdf/qtest/qpdf/qjson-bad-datafile2.out
@@ -0,0 +1,2 @@
+WARNING: qjson-bad-datafile2.json (obj:4 0 R, offset 850): "stream.datafile" must be a string containing a file name
+qpdf: qjson-bad-datafile2.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out
index f364f1a6..476128e7 100644
--- a/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version1.out
@@ -1,3 +1,3 @@
-WARNING: qjson-bad-pdf-version1.json (offset 41): invalid JSON version (must be 2)
-WARNING: qjson-bad-pdf-version1.json (offset 70): invalid PDF version (must be x.y)
+WARNING: qjson-bad-pdf-version1.json (offset 41): invalid JSON version (must be numeric value 2)
+WARNING: qjson-bad-pdf-version1.json (offset 70): invalid PDF version (must be "x.y")
qpdf: qjson-bad-pdf-version1.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out
index 9bc88ff4..cb914414 100644
--- a/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out
+++ b/qpdf/qtest/qpdf/qjson-bad-pdf-version2.out
@@ -1,5 +1,5 @@
-WARNING: qjson-bad-pdf-version2.json (offset 41): invalid JSON version (must be 2)
-WARNING: qjson-bad-pdf-version2.json (offset 66): invalid PDF version (must be x.y)
+WARNING: qjson-bad-pdf-version2.json (offset 41): invalid JSON version (must be numeric value 2)
+WARNING: qjson-bad-pdf-version2.json (offset 66): invalid PDF version (must be "x.y")
WARNING: qjson-bad-pdf-version2.json (offset 97): calledgetallpages must be a boolean
WARNING: qjson-bad-pdf-version2.json (offset 138): pushedinheritedpageresources must be a boolean
qpdf: qjson-bad-pdf-version2.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-obj-key-errors.out b/qpdf/qtest/qpdf/qjson-obj-key-errors.out
index 0263f294..f1f0a369 100644
--- a/qpdf/qtest/qpdf/qjson-obj-key-errors.out
+++ b/qpdf/qtest/qpdf/qjson-obj-key-errors.out
@@ -1,7 +1,7 @@
WARNING: qjson-obj-key-errors.json (obj:2 0 R, offset 244): object must have exactly one of "value" or "stream"
WARNING: qjson-obj-key-errors.json (obj:3 0 R, offset 542): object must have exactly one of "value" or "stream"
-WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 710): "stream" is missing "dict"
-WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 710): new "stream" must have exactly one of "data" or "datafile"
-WARNING: qjson-obj-key-errors.json (obj:5 0 R, offset 800): new "stream" must have exactly one of "data" or "datafile"
+WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 690): "stream" is missing "dict"
+WARNING: qjson-obj-key-errors.json (obj:4 0 R, offset 690): new "stream" must have exactly one of "data" or "datafile"
+WARNING: qjson-obj-key-errors.json (obj:5 0 R, offset 780): new "stream" must have exactly one of "data" or "datafile"
WARNING: qjson-obj-key-errors.json (trailer, offset 1178): "trailer" is missing "value"
qpdf: qjson-obj-key-errors.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out
index a264839f..04df1518 100644
--- a/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out
+++ b/qpdf/qtest/qpdf/qjson-stream-dict-not-dict.out
@@ -1,5 +1,4 @@
WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 142): "stream.dict" must be a dictionary
-WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 142): unrecognized string value
-WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 122): new "stream" must have exactly one of "data" or "datafile"
+WARNING: qjson-stream-dict-not-dict.json (obj:1 0 R, offset 102): new "stream" must have exactly one of "data" or "datafile"
WARNING: qjson-stream-dict-not-dict.json: "qpdf[1].trailer" was not seen
qpdf: qjson-stream-dict-not-dict.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-stream-not-dict.out b/qpdf/qtest/qpdf/qjson-stream-not-dict.out
index fbd953c6..db775b59 100644
--- a/qpdf/qtest/qpdf/qjson-stream-not-dict.out
+++ b/qpdf/qtest/qpdf/qjson-stream-not-dict.out
@@ -1,3 +1,4 @@
WARNING: qjson-stream-not-dict.json (obj:1 0 R, offset 122): "stream" must be a dictionary
+WARNING: qjson-stream-not-dict.json (obj:1 0 R, offset 102): "stream" is missing "dict"
WARNING: qjson-stream-not-dict.json: "qpdf[1].trailer" was not seen
qpdf: qjson-stream-not-dict.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/qjson-trailer-stream.out b/qpdf/qtest/qpdf/qjson-trailer-stream.out
index a625cd6d..fccb2a39 100644
--- a/qpdf/qtest/qpdf/qjson-trailer-stream.out
+++ b/qpdf/qtest/qpdf/qjson-trailer-stream.out
@@ -1,2 +1,3 @@
WARNING: qjson-trailer-stream.json (trailer, offset 1269): the trailer may not be a stream
+WARNING: qjson-trailer-stream.json (trailer, offset 1249): "trailer" is missing "value"
qpdf: qjson-trailer-stream.json: errors found in JSON
diff --git a/qpdf/qtest/qpdf/update-from-json-errors.out b/qpdf/qtest/qpdf/update-from-json-errors.out
index 530d707d..5e136c55 100644
--- a/qpdf/qtest/qpdf/update-from-json-errors.out
+++ b/qpdf/qtest/qpdf/update-from-json-errors.out
@@ -1,4 +1,4 @@
-WARNING: good13.pdf (obj:4 0 R from qpdf-json-update-errors.json, offset 95): existing "stream" may at most one of "data" or "datafile"
+WARNING: good13.pdf (obj:4 0 R from qpdf-json-update-errors.json, offset 75): existing "stream" may at most one of "data" or "datafile"
WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 335): unrecognized string value
-WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 293): new "stream" must have exactly one of "data" or "datafile"
+WARNING: good13.pdf (obj:20 0 R from qpdf-json-update-errors.json, offset 273): new "stream" must have exactly one of "data" or "datafile"
qpdf: qpdf-json-update-errors.json: errors found in JSON