├── .codebeatignore ├── .codecov.yml ├── Tests ├── LinuxMain.swift └── DataURITests │ ├── XCTestManifests.swift │ ├── Utilities │ └── Expect.swift │ ├── DataURITests.swift │ └── ParserTests.swift ├── .swiftlint.yml ├── Sources └── DataURI │ ├── Helpers │ ├── Byte+asciiCode.swift │ ├── Array+LosslessDataConvertible.swift │ ├── Byte+Digits.swift │ ├── Byte+Controllers.swift │ └── Byte+Alphabet.swift │ ├── Sequence+makeString.swift │ ├── DataURI+String.swift │ ├── Scanner.swift │ └── Parser.swift ├── Package.swift ├── LICENSE ├── .gitignore ├── .circleci └── config.yml └── README.md /.codebeatignore: -------------------------------------------------------------------------------- 1 | Public/** 2 | Resources/Assets/** 3 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "0...100" 3 | ignore: 4 | - "Tests" 5 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import DataURITests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += DataURITests.__allTests() 7 | 8 | XCTMain(tests) 9 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Sources 3 | function_body_length: 4 | warning: 60 5 | variable_name: 6 | min_length: 7 | warning: 2 8 | line_length: 80 9 | disabled_rules: 10 | - opening_brace 11 | colon: 12 | flexible_right_spacing: true 13 | -------------------------------------------------------------------------------- /Sources/DataURI/Helpers/Byte+asciiCode.swift: -------------------------------------------------------------------------------- 1 | public typealias Byte = UInt8 2 | public typealias Bytes = [Byte] 3 | 4 | extension Byte { 5 | internal var asciiCode: Byte { 6 | if self >= 48 && self <= 57 { 7 | return self - 48 8 | } else if self >= 65 && self <= 70 { 9 | return self - 55 10 | } else { 11 | return 0 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "DataURI", 7 | products: [ 8 | .library(name: "DataURI", targets: ["DataURI"]) 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target( 13 | name: "DataURI", 14 | dependencies: [] 15 | ), 16 | .testTarget( 17 | name: "DataURITests", 18 | dependencies: ["DataURI"] 19 | ) 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /Sources/DataURI/Sequence+makeString.swift: -------------------------------------------------------------------------------- 1 | extension Sequence where Iterator.Element == Byte { 2 | /// Converts a slice of bytes to 3 | /// string. Courtesy of Vapor 2 - Core and @vzsg 4 | public func makeString() -> String { 5 | let array = Array(self) + [0] 6 | 7 | return array.withUnsafeBytes { rawBuffer in 8 | guard let pointer = rawBuffer.baseAddress?.assumingMemoryBound(to: CChar.self) else { return nil } 9 | return String(validatingUTF8: pointer) 10 | } ?? "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/DataURI/DataURI+String.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | /** 5 | Parses a Data URI and returns its data and type. 6 | 7 | - Returns: The type of the file and its data as bytes. 8 | */ 9 | public func dataURIDecoded() throws -> (data: Bytes, type: String) { 10 | let (data, type, _) = try DataURIParser.parse(uri: self) 11 | return (data, type.makeString()) 12 | } 13 | 14 | /// Converts the string to a UTF8 array of bytes. 15 | public var bytes: [UInt8] { 16 | return [UInt8](self.utf8) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/DataURI/Helpers/Array+LosslessDataConvertible.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | public protocol LosslessDataConvertible { 3 | /// Losslessly converts this type to `Data`. 4 | func convertToData() -> Data 5 | 6 | /// Losslessly converts `Data` to this type. 7 | static func convertFromData(_ data: Data) -> Self 8 | } 9 | 10 | extension Array: LosslessDataConvertible where Element == UInt8 { 11 | /// Converts this `[UInt8]` to `Data`. 12 | public func convertToData() -> Data { 13 | return Data(bytes: self) 14 | } 15 | 16 | /// Converts `Data` to `[UInt8]`. 17 | public static func convertFromData(_ data: Data) -> Array { 18 | return .init(data) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/DataURI/Helpers/Byte+Digits.swift: -------------------------------------------------------------------------------- 1 | /// Adds digit conveniences to `Byte`. 2 | extension Byte { 3 | /// Returns whether or not a given byte represents a UTF8 digit 0 through 9 4 | public var isDigit: Bool { 5 | return (.zero ... .nine).contains(self) 6 | } 7 | 8 | /// 0 in utf8 9 | public static let zero: Byte = 0x30 10 | 11 | /// 1 in utf8 12 | public static let one: Byte = 0x31 13 | 14 | /// 2 in utf8 15 | public static let two: Byte = 0x32 16 | 17 | /// 3 in utf8 18 | public static let three: Byte = 0x33 19 | 20 | /// 4 in utf8 21 | public static let four: Byte = 0x34 22 | 23 | /// 5 in utf8 24 | public static let five: Byte = 0x35 25 | 26 | /// 6 in utf8 27 | public static let six: Byte = 0x36 28 | 29 | /// 7 in utf8 30 | public static let seven: Byte = 0x37 31 | 32 | /// 8 in utf8 33 | public static let eight: Byte = 0x38 34 | 35 | /// 9 in utf8 36 | public static let nine: Byte = 0x39 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2018 Nodes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/DataURITests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | extension DataURITests { 4 | static let __allTests = [ 5 | ("testBase64Text", testBase64Text), 6 | ("testFailedInvalidScheme", testFailedInvalidScheme), 7 | ("testHTMLJavascriptText", testHTMLJavascriptText), 8 | ("testHTMLText", testHTMLText), 9 | ("testPublicInterface", testPublicInterface), 10 | ("testSpeed", testSpeed), 11 | ("testTextNoType", testTextNoType), 12 | ] 13 | } 14 | 15 | extension ParserTests { 16 | static let __allTests = [ 17 | ("testConsume", testConsume), 18 | ("testConsumePercentDecoded", testConsumePercentDecoded), 19 | ("testConsumePercentDecodedFailed", testConsumePercentDecodedFailed), 20 | ("testConsumeUntil", testConsumeUntil), 21 | ("testConsumeWhile", testConsumeWhile), 22 | ("testExtractType", testExtractType), 23 | ("testExtractTypeFailed", testExtractTypeFailed), 24 | ("testExtractTypeMetadata", testExtractTypeMetadata), 25 | ("testExtractTypeWithMetadata", testExtractTypeWithMetadata), 26 | ("testParserInit", testParserInit), 27 | ] 28 | } 29 | 30 | #if !os(macOS) 31 | public func __allTests() -> [XCTestCaseEntry] { 32 | return [ 33 | testCase(DataURITests.__allTests), 34 | testCase(ParserTests.__allTests), 35 | ] 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Tests/DataURITests/Utilities/Expect.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | func expect( 4 | toThrow expectedError: E, 5 | file: StaticString = #file, 6 | line: UInt = #line, 7 | from closure: () throws -> ReturnType 8 | ) where E: Equatable { 9 | do { 10 | let _ = try closure() 11 | XCTFail("should have thrown", file: file, line: line) 12 | } catch let error as E { 13 | XCTAssertEqual(error, expectedError) 14 | } catch { 15 | XCTFail( 16 | "expected type \(type(of: expectedError)) got \(type(of: error))", 17 | file: file, 18 | line: line 19 | ) 20 | } 21 | } 22 | 23 | func expectNoThrow( 24 | file: StaticString = #file, 25 | line: UInt = #line, 26 | _ closure: () throws -> ReturnType 27 | ) { 28 | do { 29 | let _ = try closure() 30 | } catch { 31 | XCTFail("closure threw: \(error)", file: file, line: line) 32 | } 33 | } 34 | 35 | func expect( 36 | _ closure: () throws -> ReturnType, 37 | file: StaticString = #file, 38 | line: UInt = #line, 39 | toReturn expectedResult: ReturnType 40 | ) where ReturnType: Equatable { 41 | do { 42 | let result = try closure() 43 | XCTAssertEqual(result, expectedResult, file: file, line: line) 44 | } catch { 45 | XCTFail("closure threw: \(error)", file: file, line: line) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | Packages/ 6 | *.xcodeproj 7 | 8 | ## Build generated 9 | build/ 10 | DerivedData/ 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xcuserstate 26 | test.Dockerfile 27 | dockerTest.sh 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | *.ipa 32 | *.dSYM.zip 33 | *.dSYM 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | .build/ 44 | Package.resolved 45 | 46 | # CocoaPods 47 | # 48 | # We recommend against adding the Pods directory to your .gitignore. However 49 | # you should judge for yourself, the pros and cons are mentioned at: 50 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 51 | # 52 | # Pods/ 53 | 54 | # Carthage 55 | # 56 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 57 | # Carthage/Checkouts 58 | 59 | Carthage/Build 60 | 61 | # fastlane 62 | # 63 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 64 | # screenshots whenever they are needed. 65 | # For more information about the recommended setup visit: 66 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 67 | 68 | fastlane/report.xml 69 | fastlane/Preview.html 70 | fastlane/screenshots 71 | fastlane/test_output 72 | 73 | Package.pins 74 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | MacOS: 4 | macos: 5 | xcode: "10.0" 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-spm-deps-{{ checksum "Package.swift" }} 11 | - run: 12 | name: Install dependencies 13 | command: | 14 | brew tap vapor/homebrew-tap 15 | brew install cmysql 16 | brew install ctls 17 | brew install libressl 18 | - run: 19 | name: Build and Run Tests 20 | no_output_timeout: 1800 21 | command: | 22 | swift package generate-xcodeproj --enable-code-coverage 23 | xcodebuild -scheme DataURI-Package -enableCodeCoverage YES test | xcpretty 24 | - run: 25 | name: Report coverage to Codecov 26 | command: | 27 | bash <(curl -s https://codecov.io/bash) 28 | - save_cache: 29 | key: v1-spm-deps-{{ checksum "Package.swift" }} 30 | paths: 31 | - .build 32 | Linux: 33 | docker: 34 | - image: nodesvapor/vapor-ci:swift-4.2 35 | steps: 36 | - checkout 37 | - restore_cache: 38 | keys: 39 | - v2-spm-deps-{{ checksum "Package.swift" }} 40 | - run: 41 | name: Copy Package File 42 | command: cp Package.swift res 43 | - run: 44 | name: Build and Run Tests 45 | no_output_timeout: 1800 46 | command: | 47 | swift test -Xswiftc -DNOJSON 48 | - run: 49 | name: Restoring Package File 50 | command: mv res Package.swift 51 | - save_cache: 52 | key: v2-spm-deps-{{ checksum "Package.swift" }} 53 | paths: 54 | - .build 55 | workflows: 56 | version: 2 57 | build-and-test: 58 | jobs: 59 | - MacOS 60 | - Linux 61 | experimental: 62 | notify: 63 | branches: 64 | only: 65 | - master 66 | - develop 67 | -------------------------------------------------------------------------------- /Sources/DataURI/Scanner.swift: -------------------------------------------------------------------------------- 1 | struct Scanner { 2 | var pointer: UnsafePointer 3 | var elements: UnsafeBufferPointer 4 | // assuming you don't mutate no copy _should_ occur 5 | let elementsCopy: [Element] 6 | } 7 | 8 | extension Scanner { 9 | init(_ data: [Element]) { 10 | self.elementsCopy = data 11 | self.elements = elementsCopy.withUnsafeBufferPointer { $0 } 12 | 13 | self.pointer = elements.baseAddress! 14 | } 15 | } 16 | 17 | extension Scanner { 18 | func peek(aheadBy n: Int = 0) -> Element? { 19 | guard pointer.advanced(by: n) < elements.endAddress else { return nil } 20 | return pointer.advanced(by: n).pointee 21 | } 22 | 23 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 24 | @discardableResult 25 | mutating func pop() -> Element { 26 | assert(pointer != elements.endAddress) 27 | defer { pointer = pointer.advanced(by: 1) } 28 | return pointer.pointee 29 | } 30 | 31 | /// - Precondition: index != bytes.endIndex. It is assumed before calling pop that you have 32 | @discardableResult 33 | mutating func attemptPop() throws -> Element { 34 | guard pointer < elements.endAddress else { throw ScannerError.Reason.endOfStream } 35 | defer { pointer = pointer.advanced(by: 1) } 36 | return pointer.pointee 37 | } 38 | 39 | mutating func pop(_ n: Int) { 40 | for _ in 0.. { 63 | return baseAddress!.advanced(by: endIndex) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DataURI 2 | [![Swift Version](https://img.shields.io/badge/Swift-4.2-brightgreen.svg)](http://swift.org) 3 | [![Vapor Version](https://img.shields.io/badge/Vapor-3-30B6FC.svg)](http://vapor.codes) 4 | [![Circle CI](https://circleci.com/gh/nodes-vapor/data-uri/tree/master.svg?style=shield)](https://circleci.com/gh/nodes-vapor/data-uri) 5 | [![codebeat badge](https://codebeat.co/badges/7f0cab4f-f11b-43d5-8484-bc9300c23d81)](https://codebeat.co/projects/github-com-nodes-vapor-data-uri-master) 6 | [![codecov](https://codecov.io/gh/nodes-vapor/data-uri/branch/master/graph/badge.svg)](https://codecov.io/gh/nodes-vapor/data-uri) 7 | [![Readme Score](http://readme-score-api.herokuapp.com/score.svg?url=https://github.com/nodes-vapor/data-uri)](http://clayallsopp.github.io/readme-score?url=https://github.com/nodes-vapor/data-uri) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nodes-vapor/data-uri/master/LICENSE) 9 | 10 | A pure Swift parser for Data URIs. 11 | 12 | 13 | ## 📦 Installation 14 | 15 | Update your `Package.swift` file. 16 | ```swift 17 | .package(url: "https://github.com/nodes-vapor/data-uri.git", from: "2.0.0") 18 | ``` 19 | 20 | 21 | ## Getting started 🚀 22 | 23 | There are two options for decoding a Data URI. The first is using the `String` extension and the second is by using the `DataURIParser` directly. 24 | 25 | ### The `String` method 26 | 27 | This method is by far the easiest to use. All you need to do is call `.dataURIDecoded() throws -> (data: Bytes, type: String)` on any Data URI encoded `String`. 28 | 29 | ```swift 30 | import Core //just for `Bytes.string` 31 | import DataURI 32 | 33 | let uri = "data:,Hello%2C%20World!" 34 | let (data, type) = try uri.dataURIDecoded() 35 | print(data.string) // "Hello, World!" 36 | print(type) // "text/plain;charset=US-ASCII" 37 | ``` 38 | 39 | ### The `DataURIParser` method 40 | 41 | Using the parser is a bit more involved as it returns all of its results as `Bytes`. 42 | 43 | ```swift 44 | import Core //just for `Bytes.string` 45 | import DataURI 46 | 47 | let uri = "data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E" 48 | let (data, type, metadata) = try DataURIParser.parse(uri: uri) 49 | print(data.string) // "

Hello, World!

" 50 | print(type.string) // "text/html" 51 | print(metadata == nil) // "true" 52 | ``` 53 | 54 | 55 | ## 🏆 Credits 56 | 57 | This package is developed and maintained by the Vapor team at [Nodes](https://www.nodesagency.com). 58 | The package owner for this project is [Tom](https://github.com/tomserowka). 59 | 60 | 61 | ## 📄 License 62 | 63 | This package is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT) 64 | -------------------------------------------------------------------------------- /Sources/DataURI/Helpers/Byte+Controllers.swift: -------------------------------------------------------------------------------- 1 | /// Adds control character conveniences to `Byte`. 2 | extension Byte { 3 | /// Returns whether or not the given byte can be considered UTF8 whitespace 4 | public var isWhitespace: Bool { 5 | return self == .space || self == .newLine || self == .carriageReturn || self == .horizontalTab 6 | } 7 | 8 | /// '\t' 9 | public static let horizontalTab: Byte = 0x9 10 | 11 | /// '\n' 12 | public static let newLine: Byte = 0xA 13 | 14 | /// '\r' 15 | public static let carriageReturn: Byte = 0xD 16 | 17 | /// ' ' 18 | public static let space: Byte = 0x20 19 | 20 | /// ! 21 | public static let exclamation: Byte = 0x21 22 | 23 | /// " 24 | public static let quote: Byte = 0x22 25 | 26 | /// # 27 | public static let numberSign: Byte = 0x23 28 | 29 | /// $ 30 | public static let dollar: Byte = 0x24 31 | 32 | /// % 33 | public static let percent: Byte = 0x25 34 | 35 | /// & 36 | public static let ampersand: Byte = 0x26 37 | 38 | /// ' 39 | public static let apostrophe: Byte = 0x27 40 | 41 | /// ( 42 | public static let leftParenthesis: Byte = 0x28 43 | 44 | /// ) 45 | public static let rightParenthesis: Byte = 0x29 46 | 47 | /// * 48 | public static let asterisk: Byte = 0x2A 49 | 50 | /// + 51 | public static let plus: Byte = 0x2B 52 | 53 | /// , 54 | public static let comma: Byte = 0x2C 55 | 56 | /// - 57 | public static let hyphen: Byte = 0x2D 58 | 59 | /// . 60 | public static let period: Byte = 0x2E 61 | 62 | /// / 63 | public static let forwardSlash: Byte = 0x2F 64 | 65 | /// \ 66 | public static let backSlash: Byte = 0x5C 67 | 68 | /// : 69 | public static let colon: Byte = 0x3A 70 | 71 | /// ; 72 | public static let semicolon: Byte = 0x3B 73 | 74 | /// = 75 | public static let equals: Byte = 0x3D 76 | 77 | /// ? 78 | public static let questionMark: Byte = 0x3F 79 | 80 | /// @ 81 | public static let at: Byte = 0x40 82 | 83 | /// [ 84 | public static let leftSquareBracket: Byte = 0x5B 85 | 86 | /// ] 87 | public static let rightSquareBracket: Byte = 0x5D 88 | 89 | /// ^ 90 | public static let caret: Byte = 0x5E 91 | 92 | /// _ 93 | public static let underscore: Byte = 0x5F 94 | 95 | /// ` 96 | public static let backtick: Byte = 0x60 97 | 98 | /// ~ 99 | public static let tilde: Byte = 0x7E 100 | 101 | /// { 102 | public static let leftCurlyBracket: Byte = 0x7B 103 | 104 | /// } 105 | public static let rightCurlyBracket: Byte = 0x7D 106 | 107 | /// < 108 | public static let lessThan: Byte = 0x3C 109 | 110 | /// > 111 | public static let greaterThan: Byte = 0x3E 112 | 113 | /// | 114 | public static let pipe: Byte = 0x7C 115 | } 116 | 117 | extension Byte { 118 | /// Defines the `crlf` used to denote line breaks in HTTP and many other formatters 119 | public static let crlf: Bytes = [ 120 | .carriageReturn, 121 | .newLine 122 | ] 123 | } 124 | -------------------------------------------------------------------------------- /Tests/DataURITests/DataURITests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | //import Core 3 | 4 | @testable import DataURI 5 | 6 | class DataURITests: XCTestCase { 7 | static var allTests = [ 8 | ("testTextNoType", testTextNoType), 9 | ("testBase64Text", testBase64Text), 10 | ("testHTMLText", testHTMLText), 11 | ("testHTMLJavascriptText", testHTMLJavascriptText), 12 | ("testFailedInvalidScheme", testFailedInvalidScheme), 13 | ("testPublicInterface", testPublicInterface), 14 | ("testSpeed", testSpeed) 15 | ] 16 | 17 | func testTextNoType() { 18 | let (data, type, meta) = try! DataURIParser.parse( 19 | uri: "data:,Hello%2C%20World!" 20 | ) 21 | 22 | XCTAssertEqual(data.makeString(), "Hello, World!") 23 | XCTAssertEqual(type.makeString(), "text/plain;charset=US-ASCII") 24 | XCTAssertNil(meta) 25 | } 26 | 27 | func testBase64Text() { 28 | let (data, type, meta) = try! DataURIParser.parse( 29 | uri: "data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D" 30 | ) 31 | 32 | XCTAssertEqual(data.makeString(), "Hello, World!") 33 | XCTAssertEqual(type.makeString(), "text/plain") 34 | XCTAssertEqual(meta?.makeString(), "base64") 35 | } 36 | 37 | func testBase64Binary() { 38 | let (data, type, meta) = try! DataURIParser.parse( 39 | uri: "data:text/plain;base64,AAECA3Rlc3QK" 40 | ) 41 | 42 | XCTAssertEqual(data, [0, 1, 2, 3, 116, 101, 115, 116, 10]) 43 | XCTAssertEqual(type.makeString(), "text/plain") 44 | XCTAssertEqual(meta?.makeString(), "base64") 45 | } 46 | 47 | func testHTMLText() { 48 | let (data, type, meta) = try! DataURIParser.parse( 49 | uri: "data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E" 50 | ) 51 | 52 | XCTAssertEqual(data.makeString(), "

Hello, World!

") 53 | XCTAssertEqual(type.makeString(), "text/html") 54 | XCTAssertNil(meta) 55 | } 56 | 57 | func testHTMLJavascriptText() { 58 | let (data, type, meta) = try! DataURIParser.parse( 59 | uri: "data:text/html," 60 | ) 61 | 62 | XCTAssertEqual(data.makeString(), "") 63 | XCTAssertEqual(type.makeString(), "text/html") 64 | XCTAssertNil(meta) 65 | } 66 | 67 | func testFailedInvalidScheme() { 68 | expect(toThrow: DataURIParser.Error.invalidScheme) { 69 | try DataURIParser.parse(uri: "date:") 70 | } 71 | } 72 | 73 | func testPublicInterface() { 74 | expectNoThrow() { 75 | let (data, type) = try "data:,Hello%2C%20World!".dataURIDecoded() 76 | XCTAssertEqual(data.makeString(), "Hello, World!") 77 | XCTAssertEqual(type, "text/plain;charset=US-ASCII") 78 | } 79 | } 80 | 81 | func testSpeed() { 82 | measure { 83 | for _ in 0..<10_000 { 84 | _ = try! DataURIParser.parse( 85 | uri: "data:,Hello%2C%20World!" 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/DataURI/Helpers/Byte+Alphabet.swift: -------------------------------------------------------------------------------- 1 | /// Adds alphabet conveniences to `Byte`. 2 | extension Byte { 3 | /// Returns true if the given byte is between lowercase or uppercase A-Z in UTF8. 4 | public var isLetter: Bool { 5 | return (self >= .a && self <= .z) || (self >= .A && self <= .Z) 6 | } 7 | 8 | /// Returns whether or not a given byte represents a UTF8 digit 0 through 9, or an arabic letter 9 | public var isAlphanumeric: Bool { 10 | return isLetter || isDigit 11 | } 12 | 13 | /// Returns whether a given byte can be interpreted as a hex value in UTF8, ie: 0-9, a-f, A-F. 14 | public var isHexDigit: Bool { 15 | return (self >= .zero && self <= .nine) || (self >= .A && self <= .F) || (self >= .a && self <= .f) 16 | } 17 | 18 | /// A 19 | public static let A: Byte = 0x41 20 | 21 | /// B 22 | public static let B: Byte = 0x42 23 | 24 | /// C 25 | public static let C: Byte = 0x43 26 | 27 | /// D 28 | public static let D: Byte = 0x44 29 | 30 | /// E 31 | public static let E: Byte = 0x45 32 | 33 | /// F 34 | public static let F: Byte = 0x46 35 | 36 | /// G 37 | public static let G: Byte = 0x47 38 | 39 | /// H 40 | public static let H: Byte = 0x48 41 | 42 | /// I 43 | public static let I: Byte = 0x49 44 | 45 | /// J 46 | public static let J: Byte = 0x4A 47 | 48 | /// K 49 | public static let K: Byte = 0x4B 50 | 51 | /// L 52 | public static let L: Byte = 0x4C 53 | 54 | /// M 55 | public static let M: Byte = 0x4D 56 | 57 | /// N 58 | public static let N: Byte = 0x4E 59 | 60 | /// O 61 | public static let O: Byte = 0x4F 62 | 63 | /// P 64 | public static let P: Byte = 0x50 65 | 66 | /// Q 67 | public static let Q: Byte = 0x51 68 | 69 | /// R 70 | public static let R: Byte = 0x52 71 | 72 | /// S 73 | public static let S: Byte = 0x53 74 | 75 | /// T 76 | public static let T: Byte = 0x54 77 | 78 | /// U 79 | public static let U: Byte = 0x55 80 | 81 | /// V 82 | public static let V: Byte = 0x56 83 | 84 | /// W 85 | public static let W: Byte = 0x57 86 | 87 | /// X 88 | public static let X: Byte = 0x58 89 | 90 | /// Y 91 | public static let Y: Byte = 0x59 92 | 93 | /// Z 94 | public static let Z: Byte = 0x5A 95 | } 96 | 97 | extension Byte { 98 | /// a 99 | public static let a: Byte = 0x61 100 | 101 | /// b 102 | public static let b: Byte = 0x62 103 | 104 | /// c 105 | public static let c: Byte = 0x63 106 | 107 | /// d 108 | public static let d: Byte = 0x64 109 | 110 | /// e 111 | public static let e: Byte = 0x65 112 | 113 | /// f 114 | public static let f: Byte = 0x66 115 | 116 | /// g 117 | public static let g: Byte = 0x67 118 | 119 | /// h 120 | public static let h: Byte = 0x68 121 | 122 | /// i 123 | public static let i: Byte = 0x69 124 | 125 | /// j 126 | public static let j: Byte = 0x6A 127 | 128 | /// k 129 | public static let k: Byte = 0x6B 130 | 131 | /// l 132 | public static let l: Byte = 0x6C 133 | 134 | /// m 135 | public static let m: Byte = 0x6D 136 | 137 | /// n 138 | public static let n: Byte = 0x6E 139 | 140 | /// o 141 | public static let o: Byte = 0x6F 142 | 143 | /// p 144 | public static let p: Byte = 0x70 145 | 146 | /// q 147 | public static let q: Byte = 0x71 148 | 149 | /// r 150 | public static let r: Byte = 0x72 151 | 152 | /// s 153 | public static let s: Byte = 0x73 154 | 155 | /// t 156 | public static let t: Byte = 0x74 157 | 158 | /// u 159 | public static let u: Byte = 0x75 160 | 161 | /// v 162 | public static let v: Byte = 0x76 163 | 164 | /// w 165 | public static let w: Byte = 0x77 166 | 167 | /// x 168 | public static let x: Byte = 0x78 169 | 170 | /// y 171 | public static let y: Byte = 0x79 172 | 173 | /// z 174 | public static let z: Byte = 0x7A 175 | } 176 | 177 | -------------------------------------------------------------------------------- /Sources/DataURI/Parser.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// A parser for decoding Data URIs. 4 | public struct DataURIParser { 5 | enum Error: Swift.Error { 6 | case invalidScheme 7 | case invalidURI 8 | } 9 | var scanner: Scanner 10 | 11 | init(scanner: Scanner) { 12 | self.scanner = scanner 13 | } 14 | } 15 | 16 | extension DataURIParser { 17 | /** 18 | Parses a Data URI and returns its type and data. 19 | 20 | - Parameters: 21 | - uri: The URI to be parsed. 22 | 23 | - Returns: (data: Bytes, type: Bytes, typeMetadata: Bytes?) 24 | */ 25 | public static func parse(uri: String) throws -> (Bytes, Bytes, Bytes?) { 26 | guard uri.hasPrefix("data:") else { 27 | throw Error.invalidScheme 28 | } 29 | 30 | var scanner = Scanner(uri.bytes) 31 | //pop scheme ("data:") 32 | scanner.pop(5) 33 | 34 | var parser = DataURIParser(scanner: scanner) 35 | var (type, typeMetadata) = try parser.extractType() 36 | var data = try parser.extractData() 37 | 38 | //Required by RFC 2397 39 | if type.isEmpty { 40 | type = "text/plain;charset=US-ASCII".bytes 41 | } 42 | 43 | if typeMetadata == "base64".bytes, 44 | let decodedData = Data(base64Encoded: data.convertToData()) 45 | { 46 | data = Array(decodedData) 47 | } 48 | 49 | return (data, type, typeMetadata) 50 | } 51 | } 52 | 53 | extension DataURIParser { 54 | mutating func extractType() throws -> (Bytes, Bytes?) { 55 | let type = consume(until: [.comma, .semicolon]) 56 | 57 | guard let byte = scanner.peek() else { 58 | throw Error.invalidURI 59 | } 60 | 61 | var typeMetadata: Bytes? = nil 62 | 63 | if byte == .semicolon { 64 | typeMetadata = try extractTypeMetadata() 65 | } 66 | 67 | return (type, typeMetadata) 68 | } 69 | 70 | mutating func extractTypeMetadata() throws -> Bytes { 71 | assert(scanner.peek() == .semicolon) 72 | scanner.pop() 73 | 74 | return consume(until: [.comma]) 75 | } 76 | 77 | mutating func extractData() throws -> Bytes { 78 | assert(scanner.peek() == .comma) 79 | scanner.pop() 80 | return try consumePercentDecoded() 81 | } 82 | } 83 | 84 | extension DataURIParser { 85 | @discardableResult 86 | mutating func consume() -> Bytes { 87 | var bytes: Bytes = [] 88 | 89 | while let byte = scanner.peek() { 90 | scanner.pop() 91 | bytes.append(byte) 92 | } 93 | 94 | return bytes 95 | } 96 | 97 | @discardableResult 98 | mutating func consumePercentDecoded() throws -> Bytes { 99 | var bytes: Bytes = [] 100 | 101 | while var byte = scanner.peek() { 102 | if byte == .percent { 103 | byte = try decodePercentEncoding() 104 | } 105 | 106 | scanner.pop() 107 | bytes.append(byte) 108 | } 109 | 110 | return bytes 111 | } 112 | 113 | @discardableResult 114 | mutating func consume(until terminators: Set) -> Bytes { 115 | var bytes: Bytes = [] 116 | 117 | while let byte = scanner.peek(), !terminators.contains(byte) { 118 | scanner.pop() 119 | bytes.append(byte) 120 | } 121 | 122 | return bytes 123 | } 124 | 125 | @discardableResult 126 | mutating func consume(while conditional: (Byte) -> Bool) -> Bytes { 127 | var bytes: Bytes = [] 128 | 129 | while let byte = scanner.peek(), conditional(byte) { 130 | scanner.pop() 131 | bytes.append(byte) 132 | } 133 | 134 | return bytes 135 | } 136 | } 137 | 138 | extension DataURIParser { 139 | mutating func decodePercentEncoding() throws -> Byte { 140 | assert(scanner.peek() == .percent) 141 | 142 | guard 143 | let leftMostDigit = scanner.peek(aheadBy: 1), 144 | let rightMostDigit = scanner.peek(aheadBy: 2) 145 | else { 146 | throw Error.invalidURI 147 | } 148 | 149 | scanner.pop(2) 150 | 151 | return (leftMostDigit.asciiCode * 16) + rightMostDigit.asciiCode 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Tests/DataURITests/ParserTests.swift: -------------------------------------------------------------------------------- 1 | //import Core 2 | import XCTest 3 | 4 | @testable import DataURI 5 | 6 | class ParserTests: XCTestCase { 7 | static var allTests = [ 8 | ("testParserInit", testParserInit), 9 | ("testExtractType", testExtractType), 10 | ("testExtractTypeFailed", testExtractTypeFailed), 11 | ("testExtractTypeMetadata", testExtractTypeMetadata), 12 | ("testExtractTypeWithMetadata", testExtractTypeWithMetadata), 13 | ("testConsumeUntil", testConsumeUntil), 14 | ("testConsumeWhile", testConsumeWhile), 15 | ("testConsume", testConsume), 16 | ("testConsumePercentDecoded", testConsumePercentDecoded), 17 | ("testConsumePercentDecodedFailed", testConsumePercentDecodedFailed) 18 | ] 19 | 20 | func testParserInit() { 21 | let bytes: [Byte] = [ .D, .E, .A, .D, .B, .E, .E, .F ] 22 | var parser = DataURIParser(scanner: Scanner(bytes)) 23 | 24 | //check if first bytes match 25 | XCTAssertNotNil(parser.scanner.peek()) 26 | XCTAssertEqual(parser.scanner.pop(), .D) 27 | 28 | //pop everything but the last `F` 29 | parser.scanner.pop(bytes.count - 2) 30 | XCTAssertNotNil(parser.scanner.peek()) 31 | 32 | //check the last bytes and pop 33 | XCTAssertEqual(parser.scanner.pop(), .F) 34 | 35 | //assert scanner is empty 36 | XCTAssertNil(parser.scanner.peek()) 37 | } 38 | 39 | func testExtractType() { 40 | expectNoThrow(){ 41 | let bytes = "text/html,DEADBEEF".bytes 42 | var parser = DataURIParser(scanner: Scanner(bytes)) 43 | let (type, metadata) = try parser.extractType() 44 | 45 | XCTAssertEqual(type.makeString(), "text/html") 46 | XCTAssertNil(metadata) 47 | } 48 | } 49 | 50 | /** 51 | This test ensures the type is terminated by `,` or `;` and that there is 52 | data left to be parsed. 53 | */ 54 | func testExtractTypeFailed() { 55 | let bytes = "text/html".bytes 56 | var parser = DataURIParser(scanner: Scanner(bytes)) 57 | 58 | expect(toThrow: DataURIParser.Error.invalidURI) { 59 | try parser.extractType() 60 | } 61 | } 62 | 63 | func testExtractTypeMetadata() { 64 | expectNoThrow() { 65 | let bytes = ";base64,DEADBEEF".bytes 66 | var parser = DataURIParser(scanner: Scanner(bytes)) 67 | let metadata = try parser.extractTypeMetadata() 68 | 69 | XCTAssertEqual(metadata.makeString(), "base64") 70 | } 71 | } 72 | 73 | func testExtractTypeWithMetadata() { 74 | expectNoThrow() { 75 | let bytes = "text/html;base64,DEADBEEF".bytes 76 | var parser = DataURIParser(scanner: Scanner(bytes)) 77 | let (type, metadata) = try parser.extractType() 78 | 79 | XCTAssertEqual(type.makeString(), "text/html") 80 | XCTAssertNotNil(metadata) 81 | XCTAssertEqual(metadata?.makeString(), "base64") 82 | } 83 | } 84 | 85 | func testConsumeUntil() { 86 | let bytes: [Byte] = [ 87 | .a, 88 | .A, 89 | .B, 90 | .comma, 91 | .f 92 | ] 93 | 94 | let expected: [Byte] = [.a, .A, .B] 95 | 96 | var parser = DataURIParser(scanner: Scanner(bytes)) 97 | let output = parser.consume(until: [.comma]) 98 | 99 | XCTAssertEqual(output, expected) 100 | XCTAssertNotNil(parser.scanner.peek()) 101 | XCTAssertEqual(parser.scanner.pop(), Byte.comma) 102 | } 103 | 104 | func testConsumeWhile() { 105 | let bytes: [Byte] = [ 106 | .a, 107 | .a, 108 | .a, 109 | .comma, 110 | .F 111 | ] 112 | 113 | let expected: [Byte] = [.a, .a, .a] 114 | 115 | var parser = DataURIParser(scanner: Scanner(bytes)) 116 | let output = parser.consume(while: { $0 == .a }) 117 | 118 | XCTAssertEqual(output, expected) 119 | XCTAssertNotNil(parser.scanner.peek()) 120 | XCTAssertEqual(parser.scanner.pop(), Byte.comma) 121 | } 122 | 123 | func testConsume() { 124 | let bytes: [Byte] = [ 125 | .a, 126 | .C, 127 | .semicolon, 128 | .comma, 129 | .f 130 | ] 131 | 132 | let expected: [Byte] = [.a, .C, .semicolon, .comma, .f] 133 | 134 | var parser = DataURIParser(scanner: Scanner(bytes)) 135 | let output = parser.consume() 136 | 137 | XCTAssertEqual(output, expected) 138 | XCTAssertNil(parser.scanner.peek()) 139 | } 140 | 141 | func testConsumePercentDecoded() { 142 | let bytes: [Byte] = [ 143 | .a, 144 | .C, 145 | .semicolon, 146 | .comma, 147 | .percent, 148 | 0x32, //2 149 | .C, 150 | .percent, 151 | 0x32, // 2 152 | 0x00, 153 | .f 154 | ] 155 | 156 | let expected: [Byte] = [.a, .C, .semicolon, .comma, .comma, .space, .f] 157 | 158 | var parser = DataURIParser(scanner: Scanner(bytes)) 159 | let output = try! parser.consumePercentDecoded() 160 | 161 | XCTAssertEqual(output, expected) 162 | XCTAssertNil(parser.scanner.peek()) 163 | } 164 | 165 | func testConsumePercentDecodedFailed() { 166 | let bytes: [Byte] = [ 167 | .percent, 168 | .C, 169 | ] 170 | 171 | expect(toThrow: DataURIParser.Error.invalidURI) { 172 | var parser = DataURIParser(scanner: Scanner(bytes)) 173 | _ = try parser.consumePercentDecoded() 174 | } 175 | } 176 | } 177 | --------------------------------------------------------------------------------