Skip to content

Commit 252015b

Browse files
brianquinlanCommit Queue
authored and
Commit Queue
committed
[io] Fix a bug where large reads would return partial data.
Bug: #51071 Change-Id: Ia64d803c9709b106e52a1c671c1c3288c051bd85 Tested: ci + new test CoreLibraryReviewExempt: bug fix only for vm Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/279204 Reviewed-by: Alexander Aprelev <[email protected]> Reviewed-by: Martin Kustermann <[email protected]> Commit-Queue: Brian Quinlan <[email protected]>
1 parent d013119 commit 252015b

File tree

4 files changed

+218
-11
lines changed

4 files changed

+218
-11
lines changed

runtime/bin/file.h

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,15 @@ class File : public ReferenceCounted<File> {
143143
int64_t length,
144144
void* start = nullptr);
145145

146-
// Read/Write attempt to transfer num_bytes to/from buffer. It returns
147-
// the number of bytes read/written.
146+
// Read at most 'num_bytes' from the file. It may read less than 'num_bytes'
147+
// even when EOF is not encountered. If no data is available then `Read`
148+
// will block waiting for input (e.g. if the file represents a pipe that
149+
// contains no unread data). A positive return value indicates the number of
150+
// bytes read. Zero indicates an attempt to read past the end-of-file. -1
151+
// indicates an error.
148152
int64_t Read(void* buffer, int64_t num_bytes);
153+
// Attempt to write 'num_bytes' bytes from 'buffer'. It returns the number
154+
// of bytes written.
149155
int64_t Write(const void* buffer, int64_t num_bytes);
150156

151157
// ReadFully and WriteFully do attempt to transfer num_bytes to/from

sdk/lib/io/file_impl.dart

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,23 @@ part of dart.io;
77
// Read the file in blocks of size 64k.
88
const int _blockSize = 64 * 1024;
99

10+
// The maximum number of bytes to read in a single call to `read`.
11+
//
12+
// On Windows and macOS, it is an error to call
13+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX`.
14+
//
15+
// The POSIX specification states that the behavior of `read` is
16+
// implementation-defined if `nbyte > SSIZE_MAX`. On Linux, the `read` will
17+
// transfer at most 0x7ffff000 bytes and return the number of bytes actually.
18+
// transfered.
19+
//
20+
// A smaller value has the advantage of:
21+
// 1. making vm-service clients (e.g. debugger) more responsive
22+
// 2. reducing memory overhead (since `readInto` copies memory)
23+
//
24+
// A bigger value reduces the number of system calls.
25+
const int _maxReadSize = 16 * 1024 * 1024; // 16MB.
26+
1027
class _FileStream extends Stream<List<int>> {
1128
// Stream controller.
1229
late StreamController<Uint8List> _controller;
@@ -500,7 +517,7 @@ class _File extends FileSystemEntity implements File {
500517
}
501518

502519
Future<Uint8List> readAsBytes() {
503-
Future<Uint8List> readDataChunked(RandomAccessFile file) {
520+
Future<Uint8List> readUnsized(RandomAccessFile file) {
504521
var builder = new BytesBuilder(copy: false);
505522
var completer = new Completer<Uint8List>();
506523
void read() {
@@ -518,13 +535,37 @@ class _File extends FileSystemEntity implements File {
518535
return completer.future;
519536
}
520537

538+
Future<Uint8List> readSized(RandomAccessFile file, int length) {
539+
var data = Uint8List(length);
540+
var offset = 0;
541+
var completer = new Completer<Uint8List>();
542+
void read() {
543+
file.readInto(data, offset, min(offset + _maxReadSize, length)).then(
544+
(readSize) {
545+
if (readSize > 0) {
546+
offset += readSize;
547+
read();
548+
} else {
549+
assert(readSize == 0);
550+
if (offset < length) {
551+
data = Uint8List.sublistView(data, 0, offset);
552+
}
553+
completer.complete(data);
554+
}
555+
}, onError: completer.completeError);
556+
}
557+
558+
read();
559+
return completer.future;
560+
}
561+
521562
return open().then((file) {
522563
return file.length().then((length) {
523564
if (length == 0) {
524565
// May be character device, try to read it in chunks.
525-
return readDataChunked(file);
566+
return readUnsized(file);
526567
}
527-
return file.read(length);
568+
return readSized(file, length);
528569
}).whenComplete(file.close);
529570
});
530571
}
@@ -539,11 +580,27 @@ class _File extends FileSystemEntity implements File {
539580
var builder = new BytesBuilder(copy: false);
540581
do {
541582
data = opened.readSync(_blockSize);
542-
if (data.length > 0) builder.add(data);
583+
if (data.length > 0) {
584+
builder.add(data);
585+
}
543586
} while (data.length > 0);
544587
data = builder.takeBytes();
545588
} else {
546-
data = opened.readSync(length);
589+
data = Uint8List(length);
590+
var offset = 0;
591+
592+
while (offset < length) {
593+
final readSize = opened.readIntoSync(
594+
data, offset, min(offset + _maxReadSize, length));
595+
if (readSize == 0) {
596+
break;
597+
}
598+
offset += readSize;
599+
}
600+
601+
if (offset < length) {
602+
data = Uint8List.view(data.buffer, 0, offset);
603+
}
547604
}
548605
return data;
549606
} finally {

tests/standalone/io/file_test.dart

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import 'dart:async';
1414
import 'dart:convert';
1515
import 'dart:collection';
1616
import 'dart:io';
17+
import 'dart:typed_data';
1718

1819
import "package:async_helper/async_helper.dart";
1920
import "package:expect/expect.dart";
@@ -41,6 +42,10 @@ class MyListOfOneElement extends Object
4142

4243
class FileTest {
4344
static late Directory tempDirectory;
45+
static late File largeFile;
46+
static final largeFileLastBytes = [104, 101, 108, 108, 111];
47+
static final largeFileSize = (1 << 31) + largeFileLastBytes.length;
48+
4449
static int numLiveAsyncTests = 0;
4550

4651
static void asyncTestStarted() {
@@ -63,6 +68,26 @@ class FileTest {
6368
});
6469
}
6570

71+
static void createLargeFile(Function doNext) {
72+
// Increase the test count to prevent the temp directory from being
73+
// deleted.
74+
++numLiveAsyncTests;
75+
final buffer = Uint8List(1 << 24);
76+
largeFile = new File('${tempDirectory.path}/test_large_file');
77+
IOSink output = largeFile.openWrite();
78+
for (var i = 0;
79+
i < (largeFileSize - largeFileLastBytes.length) / buffer.length;
80+
++i) {
81+
output.add(buffer);
82+
}
83+
output.add(largeFileLastBytes);
84+
output.flush().then((_) => output.close());
85+
output.done.then((_) {
86+
--numLiveAsyncTests;
87+
doNext();
88+
});
89+
}
90+
6691
static void deleteTempDirectory() {
6792
tempDirectory.deleteSync(recursive: true);
6893
}
@@ -1132,6 +1157,27 @@ class FileTest {
11321157
});
11331158
}
11341159

1160+
static void testReadAsBytesLargeFile() {
1161+
asyncTestStarted();
1162+
// On Windows and macOS, it is an error to call
1163+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX` and, on Linux,
1164+
// it returns partial data.
1165+
largeFile.readAsBytes().then((bytes) {
1166+
Expect.equals(largeFileSize, bytes.length);
1167+
Expect.listEquals(
1168+
largeFileLastBytes,
1169+
Uint8List.sublistView(
1170+
bytes, bytes.length - largeFileLastBytes.length));
1171+
asyncTestDone("testReadAsBytesLargeFile");
1172+
}, onError: (e) {
1173+
// The test fails on 32-bit platforms or when using compressed
1174+
// pointers. It is not possible to identify when running with
1175+
// compressed pointers.
1176+
Expect.type<OutOfMemoryError>(e);
1177+
asyncTestDone("testReadAsBytesLargeFile");
1178+
});
1179+
}
1180+
11351181
static void testReadAsBytesSync() {
11361182
var name = getFilename("fixed_length_file");
11371183
var bytes = new File(name).readAsBytesSync();
@@ -1145,6 +1191,27 @@ class FileTest {
11451191
Expect.equals(bytes.length, 0);
11461192
}
11471193

1194+
static testReadAsBytesSyncLargeFile() {
1195+
asyncTestStarted();
1196+
// On Windows and macOS, it is an error to call
1197+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX` and, on Linux,
1198+
// it returns partial data.
1199+
Uint8List? bytes;
1200+
try {
1201+
bytes = largeFile.readAsBytesSync();
1202+
} on OutOfMemoryError catch (e) {
1203+
// The test fails on 32-bit platforms or when using compressed
1204+
// pointers. It is not possible to identify when running with
1205+
// compressed pointers.
1206+
asyncTestDone("testReadAsBytesSyncLargeFile");
1207+
return;
1208+
}
1209+
Expect.equals(largeFileSize, bytes.length);
1210+
Expect.listEquals(largeFileLastBytes,
1211+
Uint8List.sublistView(bytes, bytes.length - largeFileLastBytes.length));
1212+
asyncTestDone("testReadAsBytesSyncLargeFile");
1213+
}
1214+
11481215
static void testReadAsText() {
11491216
asyncTestStarted();
11501217
var name = getFilename("fixed_length_file");
@@ -1612,7 +1679,8 @@ class FileTest {
16121679

16131680
var absFile = file.absolute;
16141681
Expect.isTrue(absFile.isAbsolute);
1615-
Expect.isTrue(absFile.path.startsWith(tempDirectory.path));
1682+
Expect.isTrue(absFile.path.startsWith(tempDirectory.path),
1683+
'${absFile.path} not in ${tempDirectory.path}');
16161684

16171685
Expect.equals("content", absFile.readAsStringSync());
16181686

@@ -1729,7 +1797,11 @@ class FileTest {
17291797
testSetLastAccessedSyncDirectory();
17301798
testDoubleAsyncOperation();
17311799
testAbsolute();
1732-
asyncEnd();
1800+
createLargeFile(() {
1801+
testReadAsBytesLargeFile();
1802+
testReadAsBytesSyncLargeFile();
1803+
asyncEnd();
1804+
});
17331805
});
17341806
}
17351807
}

tests/standalone_2/io/file_test.dart

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import 'dart:async';
1616
import 'dart:convert';
1717
import 'dart:collection';
1818
import 'dart:io';
19+
import 'dart:typed_data';
1920

2021
import "package:async_helper/async_helper.dart";
2122
import "package:expect/expect.dart";
@@ -43,6 +44,10 @@ class MyListOfOneElement extends Object
4344

4445
class FileTest {
4546
static Directory tempDirectory;
47+
static File largeFile;
48+
static final largeFileLastBytes = [104, 101, 108, 108, 111];
49+
static final largeFileSize = (1 << 31) + largeFileLastBytes.length;
50+
4651
static int numLiveAsyncTests = 0;
4752

4853
static void asyncTestStarted() {
@@ -65,6 +70,26 @@ class FileTest {
6570
});
6671
}
6772

73+
static void createLargeFile(Function doNext) {
74+
// Increase the test count to prevent the temp directory from being
75+
// deleted.
76+
++numLiveAsyncTests;
77+
final buffer = Uint8List(1 << 24);
78+
largeFile = new File('${tempDirectory.path}/test_large_file');
79+
IOSink output = largeFile.openWrite();
80+
for (var i = 0;
81+
i < (largeFileSize - largeFileLastBytes.length) / buffer.length;
82+
++i) {
83+
output.add(buffer);
84+
}
85+
output.add(largeFileLastBytes);
86+
output.flush().then((_) => output.close());
87+
output.done.then((_) {
88+
--numLiveAsyncTests;
89+
doNext();
90+
});
91+
}
92+
6893
static void deleteTempDirectory() {
6994
tempDirectory.deleteSync(recursive: true);
7095
}
@@ -1136,6 +1161,27 @@ class FileTest {
11361161
});
11371162
}
11381163

1164+
static void testReadAsBytesLargeFile() {
1165+
asyncTestStarted();
1166+
// On Windows and macOS, it is an error to call
1167+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX` and, on Linux,
1168+
// it returns partial data.
1169+
largeFile.readAsBytes().then((bytes) {
1170+
Expect.equals(largeFileSize, bytes.length);
1171+
Expect.listEquals(
1172+
largeFileLastBytes,
1173+
Uint8List.sublistView(
1174+
bytes, bytes.length - largeFileLastBytes.length));
1175+
asyncTestDone("testReadAsBytesLargeFile");
1176+
}, onError: (e) {
1177+
// The test fails on 32-bit platforms or when using compressed
1178+
// pointers. It is not possible to identify when running with
1179+
// compressed pointers.
1180+
Expect.type<OutOfMemoryError>(e);
1181+
asyncTestDone("testReadAsBytesLargeFile");
1182+
});
1183+
}
1184+
11391185
static void testReadAsBytesSync() {
11401186
var name = getFilename("fixed_length_file");
11411187
var bytes = new File(name).readAsBytesSync();
@@ -1149,6 +1195,27 @@ class FileTest {
11491195
Expect.equals(bytes.length, 0);
11501196
}
11511197

1198+
static testReadAsBytesSyncLargeFile() {
1199+
asyncTestStarted();
1200+
// On Windows and macOS, it is an error to call
1201+
// `read/_read(fildes, buf, nbyte)` with `nbyte >= INT_MAX` and, on Linux,
1202+
// it returns partial data.
1203+
Uint8List bytes;
1204+
try {
1205+
bytes = largeFile.readAsBytesSync();
1206+
} on OutOfMemoryError catch (e) {
1207+
// The test fails on 32-bit platforms or when using compressed
1208+
// pointers. It is not possible to identify when running with
1209+
// compressed pointers.
1210+
asyncTestDone("testReadAsBytesSyncLargeFile");
1211+
return;
1212+
}
1213+
Expect.equals(largeFileSize, bytes.length);
1214+
Expect.listEquals(largeFileLastBytes,
1215+
Uint8List.sublistView(bytes, bytes.length - largeFileLastBytes.length));
1216+
asyncTestDone("testReadAsBytesSyncLargeFile");
1217+
}
1218+
11521219
static void testReadAsText() {
11531220
asyncTestStarted();
11541221
var name = getFilename("fixed_length_file");
@@ -1613,7 +1680,8 @@ class FileTest {
16131680

16141681
var absFile = file.absolute;
16151682
Expect.isTrue(absFile.isAbsolute);
1616-
Expect.isTrue(absFile.path.startsWith(tempDirectory.path));
1683+
Expect.isTrue(absFile.path.startsWith(tempDirectory.path),
1684+
'${absFile.path} not in ${tempDirectory.path}');
16171685

16181686
Expect.equals("content", absFile.readAsStringSync());
16191687

@@ -1730,7 +1798,11 @@ class FileTest {
17301798
testSetLastAccessedSyncDirectory();
17311799
testDoubleAsyncOperation();
17321800
testAbsolute();
1733-
asyncEnd();
1801+
createLargeFile(() {
1802+
testReadAsBytesLargeFile();
1803+
testReadAsBytesSyncLargeFile();
1804+
asyncEnd();
1805+
});
17341806
});
17351807
}
17361808
}

0 commit comments

Comments
 (0)