TCPTransports/LineFraming.swift

/*
    Copyright (C) 2018 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this sample’s licensing information
    
    Abstract:
    Frames and unframes lines.
 */
 
import Foundation
 
/// Takes an array of lines and frames them in the standard way used by Internet
/// protocols, that is, adding a CR LF at the end.
///
/// This cannot throw because, hey, you shouldn’t have given it bogus strings
/// in the first place.
///
/// - note: This is a method on a `struct` to match `LineUnframer`, which has to
///     maintain complex state.  It’s also possible to imagine a different type
///     of framer that needs to maintain state.
 
struct LineFramer {
    
    func data(for lines: [String]) -> Data {
        return lines.map { "\($0)\r\n" }.joined().data(using: .utf8)!
    }
}
 
/// Takes data and parses it looking for lines.
///
/// This has to maintain a buffer of data looking for line breaks.  You
/// initialise it with the maximum line length, measured in bytes of
/// UTF-8 encoded data.
///
/// Also, this will throw if it gets a malformed line, including:
///
/// * Code sequences that aren’t valid UTF-8
/// * The line is too long.
 
struct LineUnframer {
 
    init(maxLineLength: Int) {
        precondition(maxLineLength > 0)
        self.maxLineLength = maxLineLength
    }
 
    let maxLineLength: Int
 
    private enum State {
        case standard(Data)
        case expectingLF(Data)
        case error(Swift.Error)
    }
 
    private var state: State = .standard(Data())
    
    mutating func lines(for data: Data) throws -> [String] {
        if case .error(let error) = self.state {
            throw error
        }
 
        var result: [String] = []
 
        func extendLine(_ lineData: Data, _ bytes: [UInt8]) throws {
            if lineData.count + bytes.count > self.maxLineLength {
                throw Error(code: .lineTooLong, linesSoFar: result)
            }
            self.state = .standard(lineData + bytes)
        }
        
        for c in data {
            switch (self.state, c) {
            case (.expectingLF(let lineData), 10):
                guard let line = String(bytes: lineData, encoding: .utf8) else {
                    throw Error(code: .malformedUTF8, linesSoFar: result)
                }
                result.append(line)
                self.state = .standard(Data())
            case (.expectingLF(let lineData), let b):
                try extendLine(lineData, [13, b])
            case (.standard(let lineData), 13):
                self.state = .expectingLF(lineData)
            case (.standard(let lineData), let b):
                try extendLine(lineData, [b])
            case (.error, _):
                fatalError()
            }
        }
        return result
    }
    
    struct Error : Swift.Error, Equatable {
        enum Code {
        case lineTooLong
        case malformedUTF8
        }
        var code: Code
        var linesSoFar: [String]
        static func ==(lhs: Error, rhs: Error) -> Bool {
            return lhs.code == rhs.code && lhs.linesSoFar == rhs.linesSoFar
        }
    }
}