Ponyboy47/Pathman

View on GitHub
Sources/Pathman/CharacterPath/CharacterPath+Readable.swift

Summary

Maintainability
D
2 days
Test Coverage
#if os(Linux)
import func Glibc.clearerr
import let Glibc.EOF
import func Glibc.feof
import func Glibc.fgetc
import func Glibc.fread
import func Glibc.getline
import func Glibc.ungetc
#else
import func Darwin.clearerr
import let Darwin.EOF
import func Darwin.feof
import func Darwin.fgetc
import func Darwin.fread
import func Darwin.getline
import func Darwin.ungetc
#endif
/// The C function used to read from an opened file descriptor
private let cReadCharacter = fread
private let cIsEOF = feof
private let cClearError = clearerr
private let cGetCharacter = fgetc
private let cUngetCharacter = ungetc
private let cGetLine = getline

import struct Foundation.Data

private var _buffers: [CharacterPath: UnsafeMutableRawPointer] = [:]
private var _bufferSizes: [CharacterPath: Int] = [:]

private var alignment = MemoryLayout<CChar>.alignment

extension CharacterPath: CharacterReadableByOpened, LineReadableByOpened, DefaultReadByteCount {
    /// The buffer used to store data read from a path
    var buffer: UnsafeMutableRawPointer? {
        get { return _buffers[self] }
        nonmutating set {
            buffer?.deallocate()

            guard let newBuffer = newValue else {
                _buffers.removeValue(forKey: self)
                return
            }

            _buffers[self] = newBuffer
        }
    }

    /// The size of the buffer used to store read data
    var bufferSize: Int? {
        get { return _bufferSizes[self] }
        nonmutating set {
            guard let newSize = newValue else {
                _bufferSizes.removeValue(forKey: self)
                return
            }

            buffer = UnsafeMutableRawPointer.allocate(byteCount: newSize, alignment: alignment)
            _bufferSizes[self] = newSize
        }
    }

    /**
     Read data from a descriptor

     - Parameter sizeToRead: The number of bytes to read from the descriptor
     - Returns: The Data read from the descriptor

     - Throws: `ReadError.wouldBlock` when the file was opened with the `.nonBlock` flag and the read operation would
               block
     - Throws: `ReadError.badCharacterDescriptor` when the underlying file descriptor is invalid or not opened
     - Throws: `ReadError.badBufferAddress` when the buffer points to a location outside you accessible address space
     - Throws: `ReadError.interruptedBySignal` when the API call was interrupted by a signal handler
     - Throws: `ReadError.cannotReadCharacterDescriptor` when the underlying file descriptor is attached to a path which is
               unsuitable for reading or the file was opened with the `.direct` flag and either the buffer addres, the
               byteCount, or the offset are not suitably aligned
     - Throws: `ReadError.ioError` when an I/O error occured during the API call
     */
    public static func read(bytes sizeToRead: ByteRepresentable = CharacterPath.defaultByteCount,
                            from opened: Open<CharacterPath>) throws -> Data {
        guard let descriptor = opened.descriptor else {
            throw ClosedDescriptorError.alreadyClosed
        }

        // If we don't have permissions to read then throw
        guard opened.mayRead else {
            throw ReadError.cannotReadFileStream
        }

        // If there's nothing to read, just return
        guard opened.size > 0 else { return Data() }

        let bytes = sizeToRead.bytes
        let bytesToRead = bytes > opened.size ? Int(opened.size) : bytes

        // If we haven't allocated a buffer before, then allocate one now
        if opened.path.buffer == nil {
            opened.path.bufferSize = bytesToRead
            // If the buffer size is less than bytes we're going to read then reallocate the buffer
        } else if let bSize = opened.path.bufferSize, bSize < bytesToRead {
            opened.path.bufferSize = bytesToRead
        }

        // Reading the file returns the number of bytes read (or 0 if there was an error or the eof was encountered)
        let bytesRead = cReadCharacter(opened.path.buffer!, 1, bytesToRead, descriptor)
        guard bytesRead != 0 || cIsEOF(descriptor) != 0 else {
            cClearError(descriptor)
            throw ReadError()
        }

        // Return the Data read from the descriptor
        return Data(bytes: opened.path.buffer!, count: bytesRead)
    }

    public static func nextLine(strippingNewline: Bool = true, from opened: Open<CharacterPath>) throws -> Data {
        guard let descriptor = opened.descriptor else {
            throw ClosedDescriptorError.alreadyClosed
        }

        var buffer: [UnsafeMutablePointer<CChar>?] = [nil]
        var size = 0
        let bytesRead = cGetLine(&buffer, &size, descriptor)

        // swiftlint:disable identifier_name
        guard let _bytes = buffer.first, let bytes = _bytes else { return Data() }
        // swiftlint:enable identifier_name
        return Data(bytes: bytes, count: strippingNewline ? bytesRead - 1 : bytesRead)
    }

    public static func nextCharacter(from opened: Open<CharacterPath>) throws -> Character {
        guard let descriptor = opened.descriptor else {
            throw ClosedDescriptorError.alreadyClosed
        }

        // If we don't have permissions to read then throw
        guard opened.mayRead else {
            throw ReadError.cannotReadFileStream
        }

        let char = cGetCharacter(descriptor)

        guard char != EOF else {
            throw ReadError()
        }

        guard let scalar = Unicode.Scalar(Int(char)) else {
            throw CharacterError.invalidUnicodeScalar(char)
        }

        return Character(scalar)
    }

    public static func ungetCharacter(_ character: Character, to opened: Open<CharacterPath>) throws {
        guard let descriptor = opened.descriptor else {
            throw ClosedDescriptorError.alreadyClosed
        }

        // If we don't have permissions to write then throw
        guard opened.mayWrite else {
            throw WriteError.cannotWriteToFileStream
        }

        for char in character.unicodeScalars {
            let intValue = Int32(char.value)
            guard cUngetCharacter(intValue, descriptor) == intValue else {
                throw WriteError()
            }
        }
    }
}