Skip to content

Commit b7e6cdb

Browse files
ursmclaude
andauthored
Fix read() not handling partial reads for large files (ohler55#1004)
The read() system call may return fewer bytes than requested, especially for large files. Both saj.c and parse.c treated a short read as a failure, raising an IOError. This change loops read() until all bytes are consumed. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0ecfec5 commit b7e6cdb

3 files changed

Lines changed: 53 additions & 6 deletions

File tree

ext/oj/parse.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,11 +1193,19 @@ oj_pi_parse(int argc, VALUE *argv, ParseInfo pi, char *json, size_t len, int yie
11931193
buf = OJ_R_ALLOC_N(char, len + 1);
11941194
pi->json = buf;
11951195
pi->end = buf + len;
1196-
if (0 >= (cnt = read(fd, (char *)pi->json, len)) || cnt != (ssize_t)len) {
1197-
if (0 != buf) {
1198-
OJ_R_FREE(buf);
1196+
{
1197+
size_t total = 0;
1198+
1199+
while (total < len) {
1200+
cnt = read(fd, (char *)pi->json + total, len - total);
1201+
if (cnt <= 0) {
1202+
if (0 != buf) {
1203+
OJ_R_FREE(buf);
1204+
}
1205+
rb_raise(rb_eIOError, "failed to read from IO Object.");
1206+
}
1207+
total += cnt;
11991208
}
1200-
rb_raise(rb_eIOError, "failed to read from IO Object.");
12011209
}
12021210
((char *)pi->json)[len] = '\0';
12031211
/* skip UTF-8 BOM if present */

ext/oj/saj.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,8 +648,16 @@ oj_saj_parse(int argc, VALUE *argv, VALUE self) {
648648
len = lseek(fd, 0, SEEK_END);
649649
lseek(fd, 0, SEEK_SET);
650650
json = OJ_R_ALLOC_N(char, len + 1);
651-
if (0 >= (cnt = read(fd, json, len)) || cnt != (ssize_t)len) {
652-
rb_raise(rb_eIOError, "failed to read from IO Object.");
651+
{
652+
size_t total = 0;
653+
654+
while (total < len) {
655+
cnt = read(fd, json + total, len - total);
656+
if (cnt <= 0) {
657+
rb_raise(rb_eIOError, "failed to read from IO Object.");
658+
}
659+
total += cnt;
660+
}
653661
}
654662
json[len] = '\0';
655663
#endif

test/test_saj.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,37 @@ def test_full
175175
[:hash_end, nil]], handler.calls)
176176
end
177177

178+
def test_file
179+
handler = AllSaj.new()
180+
filename = File.join(__dir__, 'saj_file_test.json')
181+
File.write(filename, $json)
182+
183+
File.open(filename) do |f|
184+
Oj.saj_parse(handler, f)
185+
end
186+
187+
assert_equal([[:hash_start, nil],
188+
[:array_start, 'array'],
189+
[:hash_start, nil],
190+
[:add_value, 3, 'num'],
191+
[:add_value, 'message', 'string'],
192+
[:hash_start, 'hash'],
193+
[:hash_start, 'h2'],
194+
[:array_start, 'a'],
195+
[:add_value, 1, nil],
196+
[:add_value, 2, nil],
197+
[:add_value, 3, nil],
198+
[:array_end, 'a'],
199+
[:hash_end, 'h2'],
200+
[:hash_end, 'hash'],
201+
[:hash_end, nil],
202+
[:array_end, 'array'],
203+
[:add_value, true, 'boolean'],
204+
[:hash_end, nil]], handler.calls)
205+
ensure
206+
File.delete(filename) if filename && File.exist?(filename)
207+
end
208+
178209
def test_fixnum_bad
179210
handler = AllSaj.new()
180211
json = %{12345xyz}

0 commit comments

Comments
 (0)