public void read(int streamOffset, byte[] bytes, int bytesOffset, int numToRead)
			throws IOException, CancelledException {
		if (streamOffset < 0 || streamOffset > streamLength) {
			throw new IOException("Offset out of range.");
		}
		if (numToRead > streamLength - streamOffset) {
			throw new IOException("Not enough data left.");
		}
		if (numToRead <= 0) {
			return;
		}

		int remainingByteCount = numToRead;
		int pageNumber = streamOffset >> msf.getLog2PageSize();
		int offsetIntoPage = streamOffset & msf.getPageSizeModMask();

		// Read any partial page at beginning first.
		if (offsetIntoPage != 0) {
			int firstNumToRead = Math.min(msf.getPageSize() - offsetIntoPage, remainingByteCount);
			msf.getFileReader()
					.read(pageList.get(pageNumber), offsetIntoPage, firstNumToRead, bytes,
						bytesOffset);
			if ((remainingByteCount - firstNumToRead) > remainingByteCount) {
				throw new IOException("Integer count underflow when preparing to read.");
			}
			remainingByteCount -= firstNumToRead;
			pageNumber++;
			bytesOffset += firstNumToRead;
		}
		// Read remaining pages, including last as possible partial page.
		// Outer loop iterates over possible non-sequential groups.
		while (remainingByteCount > 0) {
			msf.checkCancelled();
			// Inner loop groups together sequential pages into one big read.
			int firstSequentialPageNumber = pageList.get(pageNumber);
			int lastSequentialPageNumber = firstSequentialPageNumber;
			int numToReadInSequentialPages = 0;
			do {
				msf.checkCancelled();
				pageNumber++;
				lastSequentialPageNumber++;
				int numToReadInPage = Math.min(msf.getPageSize(), remainingByteCount);
				numToReadInSequentialPages += numToReadInPage;
				remainingByteCount -= numToReadInPage;
			}
			while (remainingByteCount > 0 && pageList.get(pageNumber) == lastSequentialPageNumber);
			msf.getFileReader()
					.read(firstSequentialPageNumber, 0, numToReadInSequentialPages, bytes,
						bytesOffset);
			bytesOffset += numToReadInSequentialPages;
		}
	}
Reads numToRead bytes from the stream starting at streamOffset within the stream.
 The bytes are written into the bytes array starting at the bytesOffset location.
 If not all bytes are available to be read, an IOException will be thrown
@param streamOffset location within the stream from where to start reading bytes
@param bytes the array into which to write the data
@param bytesOffset the location within byte array at which to start writing data
@param numToRead number of bytes to read from the stream
@throws IOException on file seek or read, invalid parameters, bad file configuration, or
 inability to read required bytes
@throws CancelledException upon user cancellation
Ghidra/Features/PDB/src/main/java/ghidra/app/util/bin/format/pdb2/pdbreader/msf/MsfStream.java