import os, memfiles, options, stew/[ptrops, ranges/ptr_arith], async_backend, buffers export options, CloseBehavior when fsAsyncSupport: # Circular type refs prevent more targeted `when` type InputStream* = ref object of RootObj vtable*: ptr InputStreamVTable # This is nil for unsafe memory inputs buffers*: PageBuffers # This is nil for unsafe memory inputs span*: PageSpan spanEndPos*: Natural closeFut*: Future[void] # This is nil before `close` is called when debugHelpers: name*: string AsyncInputStream* {.borrow: `.`.} = distinct InputStream ReadSyncProc* = proc (s: InputStream, dst: pointer, dstLen: Natural): Natural {.nimcall, gcsafe, raises: [IOError, Defect].} ReadAsyncProc* = proc (s: InputStream, dst: pointer, dstLen: Natural): Future[Natural] {.nimcall, gcsafe, raises: [IOError, Defect].} CloseSyncProc* = proc (s: InputStream) {.nimcall, gcsafe, raises: [IOError, Defect].} CloseAsyncProc* = proc (s: InputStream): Future[void] {.nimcall, gcsafe, raises: [IOError, Defect].} GetLenSyncProc* = proc (s: InputStream): Option[Natural] {.nimcall, gcsafe, raises: [IOError, Defect].} InputStreamVTable* = object readSync*: ReadSyncProc closeSync*: CloseSyncProc getLenSync*: GetLenSyncProc readAsync*: ReadAsyncProc closeAsync*: CloseAsyncProc MaybeAsyncInputStream* = InputStream | AsyncInputStream else: type InputStream* = ref object of RootObj vtable*: ptr InputStreamVTable # This is nil for unsafe memory inputs buffers*: PageBuffers # This is nil for unsafe memory inputs span*: PageSpan spanEndPos*: Natural when debugHelpers: name*: string ReadSyncProc* = proc (s: InputStream, dst: pointer, dstLen: Natural): Natural {.nimcall, gcsafe, raises: [IOError, Defect].} CloseSyncProc* = proc (s: InputStream) {.nimcall, gcsafe, raises: [IOError, Defect].} GetLenSyncProc* = proc (s: InputStream): Option[Natural] {.nimcall, gcsafe, raises: [IOError, Defect].} InputStreamVTable* = object readSync*: ReadSyncProc closeSync*: CloseSyncProc getLenSync*: GetLenSyncProc MaybeAsyncInputStream* = InputStream type LayeredInputStream* = ref object of InputStream source*: InputStream allowWaitFor*: bool InputStreamHandle* = object s*: InputStream MemFileInputStream = ref object of InputStream file: MemFile FileInputStream = ref object of InputStream file: File template Sync*(s: InputStream): InputStream = s when fsAsyncSupport: template Async*(s: InputStream): AsyncInputStream = AsyncInputStream(s) template Sync*(s: AsyncInputStream): InputStream = InputStream(s) template Async*(s: AsyncInputStream): AsyncInputStream = s proc disconnectInputDevice(s: InputStream) = # TODO # Document the behavior that closeAsync is preferred if s.vtable != nil: when fsAsyncSupport: if s.vtable.closeAsync != nil: s.closeFut = s.vtable.closeAsync(s) elif s.vtable.closeSync != nil: s.vtable.closeSync(s) else: if s.vtable.closeSync != nil: s.vtable.closeSync(s) s.vtable = nil when fsAsyncSupport: template disconnectInputDevice(s: AsyncInputStream) = disconnectInputDevice InputStream(s) proc preventFurtherReading(s: InputStream) = s.vtable = nil s.span = default(PageSpan) when fsAsyncSupport: template preventFurtherReading(s: AsyncInputStream) = preventFurtherReading InputStream(s) template makeHandle*(sp: InputStream): InputStreamHandle = let s = sp InputStreamHandle(s: s) proc close*(s: InputStream, behavior = dontWaitAsyncClose) {.raises: [IOError, Defect].} = ## Closes the stream. Any resources associated with the stream ## will be released and no further reading will be possible. ## ## If the underlying input device requires asynchronous closing ## and `behavior` is set to `waitAsyncClose`, this proc will use ## `waitFor` to block until the async operation completes. s.disconnectInputDevice() s.preventFurtherReading() when fsAsyncSupport: if s.closeFut != nil: fsTranslateErrors "Stream closing failed": if behavior == waitAsyncClose: waitFor s.closeFut else: asyncCheck s.closeFut when fsAsyncSupport: template close*(sp: AsyncInputStream) = ## Starts the asychronous closing of the stream and returns a future that ## tracks the closing operation. let s = InputStream sp disconnectInputDevice(s) preventFurtherReading(s) if s.closeFut != nil: fsAwait s.closeFut template closeNoWait*(sp: MaybeAsyncInputStream) = ## Close the stream without waiting even if's async. ## This operation will use `asyncCheck` internally to detect unhandled ## errors from the closing operation. close(InputStream(s), dontWaitAsyncClose) # TODO # The destructors are currently disabled because they seem to cause # mysterious segmentation faults related to corrupted GC internal # data structures. #[ proc `=destroy`*(h: var InputStreamHandle) {.raises: [Defect].} = if h.s != nil: if h.s.vtable != nil and h.s.vtable.closeSync != nil: try: h.s.vtable.closeSync(h.s) except IOError: # Since this is a destructor, there is not much we can do here. # If the user wanted to handle the error, they would have called # `close` manually. discard # TODO # TODO ATTENTION! # Uncommenting the following line will lead to a GC heap corruption. # Most likely this leads to Nim collecting some object prematurely. # h.s = nil # We work-around the problem through more indirect incapacitatation # of the stream object: h.s.preventFurtherReading() ]# converter implicitDeref*(h: InputStreamHandle): InputStream = ## Any `InputStreamHandle` value can be implicitly converted to an ## `InputStream` or an `AsyncInputStream` value. h.s template vtableAddr*(vtable: InputStreamVTable): ptr InputStreamVTable = # This is a simple work-around for the somewhat broken side # effects analysis of Nim - reading from global let variables # is considered a side-effect. {.noSideEffect.}: unsafeAddr vtable let memFileInputVTable = InputStreamVTable( closeSync: proc (s: InputStream) {.nimcall, gcsafe, raises: [IOError, Defect].} = try: close MemFileInputStream(s).file except OSError as err: raise newException(IOError, "Failed to close file", err) , getLenSync: proc (s: InputStream): Option[Natural] {.nimcall, gcsafe, raises: [IOError, Defect].} = some s.span.len ) proc memFileInput*(filename: string, mappedSize = -1, offset = 0): InputStreamHandle {.raises: [IOError].} = ## Creates an input stream for reading the contents of a memory-mapped file. ## ## Using this API will provide better performance than `fileInput`, ## but this comes at a cost of higher address space usage which may ## be problematic when working with extremely large files. ## ## All parameters are forwarded to Nim's memfiles.open function: ## ## ``filename`` ## The name of the file to read. ## ## ``mappedSize`` and ``offset`` ## can be used to map only a slice of the file. ## ## ``offset`` must be multiples of the PAGE SIZE of your OS ## (usually 4K or 8K, but is unique to your OS) # Nim's memfiles module will fail to map an empty file, # but we don't consider this a problem. The stream will # be in non-readable state from the start. try: let fileSize = getFileSize(filename) if fileSize == 0: return makeHandle InputStream() let memFile = memfiles.open(filename, mode = fmRead, mappedSize = mappedSize, offset = offset) head = cast[ptr byte](memFile.mem) mappedSize = memFile.size makeHandle MemFileInputStream( vtable: vtableAddr memFileInputVTable, span: PageSpan( startAddr: head, endAddr: offset(head, mappedSize)), spanEndPos: mappedSize, file: memFile) except OSError as err: raise newException(IOError, err.msg, err) func getNewSpan(s: InputStream) = let buffers = s.buffers fsAssert buffers != nil s.span = buffers.obtainReadableSpan(s.spanEndPos) func getNewSpanOrDieTrying(s: InputStream) = getNewSpan s fsAssert s.span.hasRunway proc readableNow*(s: InputStream): bool = if s.span.hasRunway: return true getNewSpan s s.span.hasRunway when fsAsyncSupport: template readableNow*(s: AsyncInputStream): bool = readableNow InputStream(s) proc readOnce*(sp: AsyncInputStream): Future[Natural] {.async.} = let s = InputStream(sp) fsAssert s.buffers != nil and s.vtable != nil result = fsAwait s.vtable.readAsync(s, nil, 0) if s.buffers.eofReached: disconnectInputDevice(s) if result > 0 and s.span.len == 0: getNewSpan s proc timeoutToNextByteImpl(s: AsyncInputStream, deadline: Future): Future[bool] {.async.} = let readFut = s.readOnce fsAwait readFut or deadline if not readFut.finished: readFut.cancel() return true else: return false template timeoutToNextByte*(sp: AsyncInputStream, deadline: Future): bool = let s = sp if readableNow(s): true else: fsAwait timeoutToNextByteImpl(s, deadline) template timeoutToNextByte*(sp: AsyncInputStream, timeout: Duration): bool = let s = sp if readableNow(s): true else: fsAwait timeoutToNextByteImpl(s, sleepAsync(timeout)) proc closeAsync*(s: AsyncInputStream) {.async.} = close s template totalUnconsumedBytes*(s: AsyncInputStream): Natural = ## Alias for InputStream.totalUnconsumedBytes totalUnconsumedBytes InputStream(s) func getBestContiguousRunway(s: InputStream): Natural = result = s.span.len if result == 0 and s.buffers != nil: getNewSpan s result = s.span.len func totalUnconsumedBytes*(s: InputStream): Natural = ## Returns the number of bytes that are currently sitting within the stream ## buffers and that can be consumed with `read` or `advance`. let localRunway = s.span.len runwayInBuffers = if s.buffers == nil: 0 elif s.buffers.fauxEofPos != 0: s.buffers.fauxEofPos - s.spanEndPos else: s.buffers.totalBufferedBytes if localRunway == 0 and runwayInBuffers > 0: getNewSpan s localRunway + runwayInBuffers proc limitReadableRange(s: InputStream, rangeLen: Natural): Natural = s.vtable = nil let runway = s.span.len if rangeLen > runway: fsAssert s.buffers != nil return s.buffers.setFauxEof(rangeLen - runway + s.spanEndPos) else: s.span.endAddr = offset(s.span.startAddr, rangeLen) let bytesToUnconsume = runway - rangeLen s.spanEndPos -= bytesToUnconsume if s.buffers != nil: s.buffers.queue.peekFirst.consumedTo -= bytesToUnconsume return s.buffers.setFauxEof(s.spanEndPos) template withReadableRange*(sp: MaybeAsyncInputStream, rangeLen: Natural, rangeStreamVarName, blk: untyped) = let s = InputStream sp vtable = s.vtable origEndAddr = s.span.endAddr origEof = limitReadableRange(s, rangeLen) try: let rangeStreamVarName {.inject.} = s blk finally: s.vtable = vtable s.span.endAddr = origEndAddr if s.buffers != nil: restoreEof(s.buffers, origEof) let fileInputVTable = InputStreamVTable( readSync: proc (s: InputStream, dst: pointer, dstLen: Natural): Natural {.nimcall, gcsafe, raises: [IOError, Defect].} = let file = FileInputStream(s).file implementSingleRead(s.buffers, dst, dstLen, {partialReadIsEof}, readStartAddr, readLen): file.readBuffer(readStartAddr, readLen) , getLenSync: proc (s: InputStream): Option[Natural] {.nimcall, gcsafe, raises: [IOError, Defect].} = let s = FileInputStream(s) runway = s.totalUnconsumedBytes let preservedPos = getFilePos(s.file) setFilePos(s.file, 0, fspEnd) let endPos = getFilePos(s.file) setFilePos(s.file, preservedPos) some Natural(endPos - preservedPos + runway) , closeSync: proc (s: InputStream) {.nimcall, gcsafe, raises: [IOError, Defect].} = try: close FileInputStream(s).file except OSError as err: raise newException(IOError, "Failed to close file", err) ) proc fileInput*(filename: string, offset = 0, pageSize = defaultPageSize): InputStreamHandle {.raises: [IOError, OSError].} = ## Creates an input stream for reading the contents of a file ## through Nim's `io` module. ## ## Parameters: ## ## ``filename`` ## The name of the file to read. ## ## ``offset`` ## Initial position in the file where reading should start. ## let file = system.open(filename, fmRead) if offset != 0: setFilePos(file, offset) makeHandle FileInputStream( vtable: vtableAddr fileInputVTable, buffers: initPageBuffers(pageSize), file: file) proc unsafeMemoryInput*(mem: openArray[byte]): InputStreamHandle = let head = unsafeAddr mem[0] makeHandle InputStream( span: PageSpan( startAddr: head, endAddr: offset(head, mem.len)), spanEndPos: mem.len) proc unsafeMemoryInput*(str: string): InputStreamHandle = unsafeMemoryInput str.toOpenArrayByte(0, str.len - 1) proc len*(s: InputStream): Option[Natural] {.raises: [Defect, IOError].} = if s.vtable == nil: some s.totalUnconsumedBytes elif s.vtable.getLenSync != nil: s.vtable.getLenSync(s) else: none Natural when fsAsyncSupport: template len*(s: AsyncInputStream): Option[Natural] = len InputStream(s) func memoryInput*(buffers: PageBuffers): InputStreamHandle = var spanEndPos = Natural 0 var span = if buffers.len == 0: default(PageSpan) else: buffers.obtainReadableSpan(spanEndPos) makeHandle InputStream(buffers: buffers, span: span, spanEndPos: spanEndPos) func memoryInput*(data: openArray[byte]): InputStreamHandle = let stream = if data.len > 0: let buffers = initPageBuffers(data.len) page = buffers.addWritablePage(data.len) pageSpan = page.fullSpan copyMem(pageSpan.startAddr, unsafeAddr data[0], data.len) InputStream(buffers: buffers, span: pageSpan, spanEndPos: data.len) else: InputStream() makeHandle stream func memoryInput*(data: openArray[char]): InputStreamHandle = memoryInput charsToBytes(data) proc resetBuffers*(s: InputStream, buffers: PageBuffers) = # This should be used only on safe memory input streams fsAssert s.vtable == nil and s.buffers != nil and buffers.len > 0 s.spanEndPos = 0 s.buffers = buffers getNewSpan s proc continueAfterRead(s: InputStream, bytesRead: Natural): bool = # Please note that this is extracted into a proc only to reduce the code # that ends up inlined into async procs by `bufferMoreDataImpl`. # The inlining itself is required to support the await-free operation of # the `readable` APIs. # The read might have been incomplete which signals the EOF of the stream. # If this is the case, we disconnect the input device which prevents any # further attempts to read from it: if s.buffers.eofReached: disconnectInputDevice(s) if bytesRead > 0: getNewSpan s return true else: return false template bufferMoreDataImpl(s, awaiter, readOp: untyped): bool = # This template is always called when the current page has been # completely exhausted. It should produce `true` if more data was # successfully buffered, so reading can continue. # # The vtable will be `nil` for a memory stream and `vtable.readOp` # will be `nil` for a memFile. If we've reached here, this is the # end of the memory buffer, so we can signal EOF: if s.buffers == nil: false else: # There might be additional pages in our buffer queue. If so, we # just jump to the next one: getNewSpan s if hasRunway(s.span): true elif s.vtable != nil and s.vtable.readOp != nil: # We ask our input device to populate our page queue with newly # read pages. The state of the queue afterwards will tell us if # the read was successful. In `continueAfterRead`, we examine if # EOF was reached, but please note that some data might have been # read anyway: continueAfterRead(s, awaiter s.vtable.readOp(s, nil, 0)) else: false proc bufferMoreDataSync(s: InputStream): bool = # This proc exists only to avoid inlining of the code of # `bufferMoreDataImpl` into `readable` (which in turn is # a template inlined in the user code). bufferMoreDataImpl(s, noAwait, readSync) template readable*(sp: InputStream): bool = ## Checks whether reading more data from the stream is possible. ## ## If there is any unconsumed data in the stream buffers, the ## operation returns `true` immediately. You can call `read` ## or `peek` afterwards to consume or examine the next byte ## in the stream. ## ## If the stream buffers are empty, the operation may block ## until more data becomes available. The end of the stream ## may be reached at this point, which will be indicated by ## a `false` return value. Any attempt to call `read` or ## `peek` afterwards is considered a `Defect`. ## ## Please note that this API is intended for stream consumers ## who need to consume the data one byte at a time. A typical ## usage will be the following: ## ## ```nim ## while stream.readable: ## case stream.peek.char ## of '"': ## parseString(stream) ## of '0'..'9': ## parseNumber(stream) ## of '\': ## discard stream.read # skip the slash ## let escapedChar = stream.read ## ``` ## ## Even though the user code consumes the data one byte at a time, ## in the majority of cases this consist of simply incrementing a ## pointer within the stream buffers. Only when the stream buffers ## are exhausted, a new read operation will be executed throught ## the stream input device which may repopulate the buffers with ## fresh data. See `Stream Pages` for futher discussion of this. # This is a template, because we want the pointer check to be # inlined at the call sites. Only if it fails, we call into the # larger non-inlined proc: let s = sp hasRunway(s.span) or bufferMoreDataSync(s) when fsAsyncSupport: template readable*(sp: AsyncInputStream): bool = ## Async version of `readable`. ## The intended API usage is the same. Instead of blocking, an async ## stream will use `await` while waiting for more data. let s = InputStream sp if hasRunway(s.span): true else: bufferMoreDataImpl(s, fsAwait, readAsync) func continueAfterReadN(s: InputStream, runwayBeforeRead, bytesRead: Natural) = if runwayBeforeRead == 0 and bytesRead > 0: getNewSpan s template readableNImpl(s, n, awaiter, readOp: untyped): bool = let runway = totalUnconsumedBytes(s) if runway >= n: true elif s.buffers == nil or s.vtable == nil or s.vtable.readOp == nil: false else: var res = false bytesRead = Natural 0 bytesDeficit = n - runway while true: bytesRead += awaiter s.vtable.readOp(s, nil, bytesDeficit) if s.buffers.eofReached: disconnectInputDevice(s) res = bytesRead >= bytesDeficit break if bytesRead >= bytesDeficit: res = true break continueAfterReadN(s, runway, bytesRead) res proc readable*(s: InputStream, n: int): bool = ## Checks whether reading `n` bytes from the input stream is possible. ## ## If there is enough unconsumed data in the stream buffers, the ## operation will return `true` immediately. You can use `read`, ## `peek`, `read(n)` or `peek(n)` afterwards to consume up to the ## number of verified bytes. Please note that consuming more bytes ## will be considered a `Defect`. ## ## If the stream buffers do not contain enough data, the operation ## may block until more data becomes available. The end of the stream ## may be reached at this point, which will be indicated by a `false` ## return value. Please note that the stream might still contain some ## unconsumed bytes after `readable(n)` returned false. You can use ## `totalUnconsumedBytes` or a combination of `readable` and `read` ## to consume the remaining bytes if desired. ## ## If possible, prefer consuming the data one byte at a time. This ## ensures the most optimal usage of the stream buffers. Even after ## calling `readable(n)`, it's still preferrable to continue with ## `read` instead of `read(n)` because the later may require the ## resulting bytes to be copied to a freshly allocated sequence. ## ## In the situation where the consumed bytes need to be copied to ## an existing external buffer, `readInto` will provide the best ## performance instead. ## ## Just like `readable`, this operation will invoke reads on the ## stream input device only when necessary. See `Stream Pages` ## for futher discussion of this. readableNImpl(s, n, noAwait, readSync) when fsAsyncSupport: template readable*(sp: AsyncInputStream, np: int): bool = ## Async version of `readable(n)`. ## The intended API usage is the same. Instead of blocking, an async ## stream will use `await` while waiting for more data. let s = InputStream sp n = np readableNImpl(s, n, fsAwait, readAsync) template peek*(sp: InputStream): byte = let s = sp if hasRunway(s.span): s.span.startAddr[] else: getNewSpanOrDieTrying s s.span.startAddr[] when fsAsyncSupport: template peek*(s: AsyncInputStream): byte = peek InputStream(s) func readFromNewSpan(s: InputStream): byte = getNewSpanOrDieTrying s result = s.span.startAddr[] bumpPointer s.span template read*(sp: InputStream): byte = let s = sp if hasRunway(s.span): let res = s.span.startAddr[] bumpPointer(s.span) res else: readFromNewSpan s when fsAsyncSupport: template read*(s: AsyncInputStream): byte = read InputStream(s) proc peekAt*(s: InputStream, pos: int): byte {.inline.} = # TODO implement page flipping let peekHead = offset(s.span.startAddr, pos) fsAssert cast[uint](peekHead) < cast[uint](s.span.endAddr) return peekHead[] when fsAsyncSupport: template peekAt*(s: AsyncInputStream, pos: int): byte = peekAt InputStream(s), pos proc advance*(s: InputStream) = if hasRunway(s.span): bumpPointer s.span else: getNewSpan s proc advance*(s: InputStream, n: Natural) = # TODO This is silly, implement it properly for i in 0 ..< n: advance s when fsAsyncSupport: template advance*(s: AsyncInputStream) = advance InputStream(s) template advance*(s: AsyncInputStream, n: Natural) = advance InputStream(s), n proc drainBuffersInto*(s: InputStream, dstAddr: ptr byte, dstLen: Natural): Natural = var dst = dstAddr remainingBytes = dstLen runway = s.span.len if runway >= remainingBytes: copyMem(dst, s.span.startAddr, remainingBytes) s.span.bumpPointer remainingBytes return dstLen elif runway > 0: copyMem(dst, s.span.startAddr, runway) dst = offset(dst, runway) remainingBytes -= runway if s.buffers != nil: # Since we reached the end of the current page, # we have to do the equivalent of `getNewSpan`: # TODO: what if the page was extended? if s.buffers.len > 0: discard s.buffers.popFirst() for page in consumePages(s.buffers): let pageStart = page.readableStart pageLen = page.writtenTo - page.consumedTo # There are two possible scenarios ahead: # 1) We'll either stop at this page in which case our span will # point to the end of the page (so, it's fully consumed) # 2) We are going to copy the entire page to the destination # buffer and we'll continue (so, it's fully consumed again) page.consumedTo = page.writtenTo if pageLen > remainingBytes: # This page has enough data to fill the rest of the buffer: copyMem(dst, pageStart, remainingBytes) # This page is partially consumed now and we must set our # span to point to its remaining contents: s.span = PageSpan(startAddr: offset(pageStart, remainingBytes), endAddr: page.readableEnd) # We also need to know how much our position in the stream # has advanced: let bytesDrainedFromBuffers = dstLen - runway s.spanEndPos += bytesDrainedFromBuffers + s.span.len # We return the length of the buffer, which means that is # has been fully populated: return dstLen else: copyMem(dst, pageStart, pageLen) remainingBytes -= pageLen dst = offset(dst, pageLen) # We've completerly drained the current span and all the buffers, # so we set the span to a pristine state that will trigger a new # read on the next interaction with the stream. s.span = default(PageSpan) # We failed to populate the entire buffer return dstLen - remainingBytes template readIntoExImpl(s: InputStream, dst: ptr byte, dstLen: Natural, awaiter, readOp: untyped): Natural = let totalBytesDrained = drainBuffersInto(s, dst, dstLen) var bytesDeficit = (dstLen - totalBytesDrained) if bytesDeficit > 0: var adjustedDst = offset(dst, totalBytesDrained) while true: if s.vtable.isNil(): break let newBytesRead = awaiter s.vtable.readOp(s, adjustedDst, bytesDeficit) s.spanEndPos += newBytesRead bytesDeficit -= newBytesRead if s.buffers.eofReached: disconnectInputDevice(s) break if bytesDeficit == 0: break adjustedDst = offset(adjustedDst, newBytesRead) dstLen - bytesDeficit proc readIntoEx*(s: InputStream, dst: var openArray[byte]): int = ## Read data into the destination buffer. ## ## Returns the number of bytes that were successfully ## written to the buffer. The function will return a ## number smaller than the buffer length only if EOF ## was reached before the buffer was fully populated. let dstAddr = addr dst[0] let dstLen = dst.len readIntoExImpl(s, dstAddr, dstLen, noAwait, readSync) proc readInto*(s: InputStream, target: var openArray[byte]): bool = ## Read data into the destination buffer. ## ## Returns `false` if EOF was reached before the buffer ## was fully populated. if you need precise information ## regarding the number of bytes read, see `readIntoEx`. s.readIntoEx(target) == target.len when fsAsyncSupport: template readIntoEx*(sp: AsyncInputStream, dst: var openArray[byte]): int = let s = InputStream(sp) # BEWARE! `openArrayToPair` here is needed to avoid # double evaluation of the `dst` expression: let (dstAddr, dstLen) = openArrayToPair(dst) readIntoExImpl(s, dstAddr, dstLen, fsAwait, readAsync) template readInto*(sp: AsyncInputStream, dst: var openArray[byte]): bool = ## Asynchronously read data into the destination buffer. ## ## Returns `false` if EOF was reached before the buffer ## was fully populated. if you need precise information ## regarding the number of bytes read, see `readIntoEx`. ## ## If there are enough bytes already buffered by the stream, ## the expression will complete immediately. ## Otherwise, it will await more bytes to become available. let s = InputStream(sp) # BEWARE! `openArrayToPair` here is needed to avoid # double evaluation of the `dst` expression: let (dstAddr, dstLen) = openArrayToPair(dst) readIntoExImpl(s, dstAddr, dstLen, fsAwait, readAsync) == dstLen template useHeapMem(_: Natural) = var buffer: seq[byte] template allocMem(n: Natural): ptr byte = buffer.setLen(n) addr buffer[0] template useStackMem(n: static Natural) = var buffer: array[n + 1, byte] template allocMem(_: Natural): ptr byte = addr buffer[0] template readNImpl(sp: InputStream, np: Natural, createAllocMemOp: untyped): openArray[byte] = let s = sp n = np runway = getBestContiguousRunway(s) # Since Nim currently doesn't allow the `makeOpenArray` calls bellow # to appear in different branches of an if statement, the code must # be written in this branch-free linear fashion. The `dataCopy` seq # may remain empty in the case where we use stack memory or return # an `openArray` from the existing span. var startAddr: ptr byte block: # This defines the `allocMem` operation used below. # See `useHeapMem` and `useStackMem` for the possible definitions. # We are creating a block scope in order to allow multiple usages # of `read` within a single scope (avoiding duplicate definitions # of `allocMem`) createAllocMemOp(np) if n > runway: startAddr = allocMem(n) let drained {.used.} = drainBuffersInto(s, startAddr, n) fsAssert drained == n else: startAddr = s.span.startAddr bumpPointer s.span, n makeOpenArray(startAddr, n) template read*(sp: InputStream, np: static Natural): openArray[byte] = const n = np when n < maxStackUsage: readNImpl(sp, n, useStackMem) else: readNImpl(sp, n, useHeapMem) template read*(s: InputStream, n: Natural): openArray[byte] = readNImpl(s, n, useHeapMem) when fsAsyncSupport: template read*(s: AsyncInputStream, n: Natural): openArray[byte] = read InputStream(s), n proc lookAheadMatch*(s: InputStream, data: openArray[byte]): bool = for i in 0 ..< data.len: if s.peekAt(i) != data[i]: return false return true when fsAsyncSupport: template lookAheadMatch*(s: AsyncInputStream, data: openArray[byte]): bool = lookAheadMatch InputStream(s) proc next*(s: InputStream): Option[byte] = if readable(s): result = some read(s) when fsAsyncSupport: template next*(sp: AsyncInputStream): Option[byte] = let s = sp if readable(s): some read(s) else: none byte proc pos*(s: InputStream): int {.inline.} = s.spanEndPos - s.span.len when fsAsyncSupport: template pos*(s: AsyncInputStream): int = pos InputStream(s)