diff --git a/Podfile b/Podfile index f7b88b7cd..120bf095d 100644 --- a/Podfile +++ b/Podfile @@ -63,7 +63,6 @@ def shared_pods pod 'Reachability', :inhibit_warnings => true pod 'YYImage', :inhibit_warnings => true pod 'ZXingObjC', '~> 3.6.4', :inhibit_warnings => true - pod 'SwiftCSV', '~> 0.5.6', :inhibit_warnings => true end target 'Signal' do diff --git a/Podfile.lock b/Podfile.lock index e807a1bdc..180975b94 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -129,7 +129,6 @@ PODS: - SQLCipher/common - SSZipArchive (2.2.3) - Starscream (3.0.6) - - SwiftCSV (0.5.6) - SwiftProtobuf (1.5.0) - YapDatabase/SQLCipher (3.1.1): - YapDatabase/SQLCipher/Core (= 3.1.1) @@ -226,7 +225,6 @@ DEPENDENCIES: - SQLCipher (>= 4.0.1) - SSZipArchive - Starscream (from `https://github.com/signalapp/Starscream.git`, branch `signal-release`) - - SwiftCSV (~> 0.5.6) - YapDatabase/SQLCipher (from `https://github.com/signalapp/YapDatabase.git`, branch `signal-release`) - YYImage - ZXingObjC (~> 3.6.4) @@ -246,7 +244,6 @@ SPEC REPOS: - SAMKeychain - SQLCipher - SSZipArchive - - SwiftCSV - SwiftProtobuf - YYImage - ZXingObjC @@ -329,12 +326,11 @@ SPEC CHECKSUMS: SQLCipher: e434ed542b24f38ea7b36468a13f9765e1b5c072 SSZipArchive: 62d4947b08730e4cda640473b0066d209ff033c9 Starscream: 8aaf1a7feb805c816d0e7d3190ef23856f6665b9 - SwiftCSV: efb4a15dd7f2f1212b3d6986f97a3369ff6c5100 SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 YapDatabase: b418a4baa6906e8028748938f9159807fd039af4 YYImage: 1e1b62a9997399593e4b9c4ecfbbabbf1d3f3b54 ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: 8803b27f35a08849d8e3e6595c88b62522a16ae7 +PODFILE CHECKSUM: c26d913ac0e7de8c047412658746634a46681fed COCOAPODS: 1.9.3 diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index fb27ed315..35dd5ffcc 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -582,6 +582,13 @@ C3638C0524C7F0B500AF29BC /* LK002RemoveFriendRequests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3638C0424C7F0B500AF29BC /* LK002RemoveFriendRequests.swift */; }; C369549D24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C369549C24D27A3500CEB4E3 /* MultiDeviceRemovalSheet.swift */; }; C36B8707243C50C60049991D /* SignalMessaging.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 453518921FC63DBF00210559 /* SignalMessaging.framework */; }; + C396DAEF2518408B00FF6DC5 /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE82518408900FF6DC5 /* ParsingState.swift */; }; + C396DAF02518408B00FF6DC5 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAE92518408A00FF6DC5 /* String+Lines.swift */; }; + C396DAF12518408B00FF6DC5 /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAEA2518408A00FF6DC5 /* EnumeratedView.swift */; }; + C396DAF22518408B00FF6DC5 /* NamedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAEB2518408A00FF6DC5 /* NamedView.swift */; }; + C396DAF32518408B00FF6DC5 /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAEC2518408A00FF6DC5 /* Description.swift */; }; + C396DAF42518408B00FF6DC5 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAED2518408B00FF6DC5 /* Parser.swift */; }; + C396DAF52518408B00FF6DC5 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = C396DAEE2518408B00FF6DC5 /* CSV.swift */; }; C39DD28824F3318C008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; }; C39DD28A24F3336E008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; }; C39DD28B24F3336F008590FC /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C39DD28724F3318C008590FC /* Colors.xcassets */; }; @@ -1373,6 +1380,13 @@ C396469D2509D3F400B0B9F5 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = translations/ja.lproj/Localizable.strings; sourceTree = ""; }; C396469E2509D40400B0B9F5 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "translations/vi-VN.lproj/Localizable.strings"; sourceTree = ""; }; C396469F2509D41100B0B9F5 /* id-ID */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "id-ID"; path = "translations/id-ID.lproj/Localizable.strings"; sourceTree = ""; }; + C396DAE82518408900FF6DC5 /* ParsingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsingState.swift; sourceTree = ""; }; + C396DAE92518408A00FF6DC5 /* String+Lines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Lines.swift"; sourceTree = ""; }; + C396DAEA2518408A00FF6DC5 /* EnumeratedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedView.swift; sourceTree = ""; }; + C396DAEB2518408A00FF6DC5 /* NamedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NamedView.swift; sourceTree = ""; }; + C396DAEC2518408A00FF6DC5 /* Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Description.swift; sourceTree = ""; }; + C396DAED2518408B00FF6DC5 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; + C396DAEE2518408B00FF6DC5 /* CSV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSV.swift; sourceTree = ""; }; C39DD28724F3318C008590FC /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Migrating Translations from Android.md"; sourceTree = ""; }; C3AECBEA24EF5244005743DE /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = translations/fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -2728,6 +2742,7 @@ isa = PBXGroup; children = ( C3DAB3232480CB2A00725F25 /* SRCopyableLabel.swift */, + C396DAE72518407300FF6DC5 /* SwiftCSV */, ); path = Dependencies; sourceTree = ""; @@ -2749,6 +2764,20 @@ path = Migrations; sourceTree = ""; }; + C396DAE72518407300FF6DC5 /* SwiftCSV */ = { + isa = PBXGroup; + children = ( + C396DAEE2518408B00FF6DC5 /* CSV.swift */, + C396DAEC2518408A00FF6DC5 /* Description.swift */, + C396DAEA2518408A00FF6DC5 /* EnumeratedView.swift */, + C396DAEB2518408A00FF6DC5 /* NamedView.swift */, + C396DAED2518408B00FF6DC5 /* Parser.swift */, + C396DAE82518408900FF6DC5 /* ParsingState.swift */, + C396DAE92518408A00FF6DC5 /* String+Lines.swift */, + ); + path = SwiftCSV; + sourceTree = ""; + }; D221A07E169C9E5E00537ABF = { isa = PBXGroup; children = ( @@ -3413,7 +3442,6 @@ "${BUILT_PRODUCTS_DIR}/SessionMetadataKit/SessionMetadataKit.framework", "${BUILT_PRODUCTS_DIR}/SessionServiceKit/SessionServiceKit.framework", "${BUILT_PRODUCTS_DIR}/Starscream/Starscream.framework", - "${BUILT_PRODUCTS_DIR}/SwiftCSV/SwiftCSV.framework", "${BUILT_PRODUCTS_DIR}/SwiftProtobuf/SwiftProtobuf.framework", "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework", "${BUILT_PRODUCTS_DIR}/YapDatabase/YapDatabase.framework", @@ -3443,7 +3471,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SessionMetadataKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SessionServiceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Starscream.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftCSV.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftProtobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YapDatabase.framework", @@ -3497,7 +3524,6 @@ "${BUILT_PRODUCTS_DIR}/SessionMetadataKit/SessionMetadataKit.framework", "${BUILT_PRODUCTS_DIR}/SessionServiceKit/SessionServiceKit.framework", "${BUILT_PRODUCTS_DIR}/Starscream/Starscream.framework", - "${BUILT_PRODUCTS_DIR}/SwiftCSV/SwiftCSV.framework", "${BUILT_PRODUCTS_DIR}/SwiftProtobuf/SwiftProtobuf.framework", "${BUILT_PRODUCTS_DIR}/YYImage/YYImage.framework", "${BUILT_PRODUCTS_DIR}/YapDatabase/YapDatabase.framework", @@ -3523,7 +3549,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SessionMetadataKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SessionServiceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Starscream.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftCSV.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftProtobuf.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YapDatabase.framework", @@ -3729,10 +3754,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C396DAF52518408B00FF6DC5 /* CSV.swift in Sources */, B8CCF63723961D6D0091D419 /* NewPrivateChatVC.swift in Sources */, 4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */, 3461293E1FD1D72B00532771 /* ExperienceUpgradeFinder.swift in Sources */, 34C4E2582118957600BEA353 /* WebRTCProto.swift in Sources */, + C396DAF12518408B00FF6DC5 /* EnumeratedView.swift in Sources */, 34D1F0BD1F8D108C0066283D /* AttachmentUploadView.m in Sources */, 452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */, 34DC9BD921543E0C00FDDCEC /* DebugContactsUtils.m in Sources */, @@ -3778,6 +3805,7 @@ 4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */, 45794E861E00620000066731 /* CallUIAdapter.swift in Sources */, 340FC8BA204DAC8D007AEB0F /* FingerprintViewScanController.m in Sources */, + C396DAF32518408B00FF6DC5 /* Description.swift in Sources */, 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */, B85357C123A1B81900AAF6CD /* SeedReminderViewDelegate.swift in Sources */, 450D19131F85236600970622 /* RemoteVideoView.m in Sources */, @@ -3804,6 +3832,7 @@ B80C6B572384A56D00FDBC8B /* DeviceLinksVC.swift in Sources */, 34A8B3512190A40E00218A25 /* MediaAlbumCellView.swift in Sources */, 34D1F0AE1F867BFC0066283D /* OWSMessageCell.m in Sources */, + C396DAF42518408B00FF6DC5 /* Parser.swift in Sources */, B8BB82AB238F669C00BA5194 /* ConversationCell.swift in Sources */, 4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */, 4CB5F26720F6E1E2004D1B42 /* MenuActionsViewController.swift in Sources */, @@ -3875,6 +3904,7 @@ C3DAB3242480CB2B00725F25 /* SRCopyableLabel.swift in Sources */, B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */, B879D44B247E1D9200DB3608 /* PathStatusView.swift in Sources */, + C396DAF02518408B00FF6DC5 /* String+Lines.swift in Sources */, B8CCF63F23975CFB0091D419 /* JoinPublicChatVC.swift in Sources */, 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, 34D8C0271ED3673300188D7C /* DebugUIMessages.m in Sources */, @@ -3896,6 +3926,7 @@ 4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */, 45FBC5D11DF8592E00E9B410 /* SignalCall.swift in Sources */, B8B26C91234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift in Sources */, + C396DAF22518408B00FF6DC5 /* NamedView.swift in Sources */, 340FC8BB204DAC8D007AEB0F /* OWSAddToContactViewController.m in Sources */, 45F32C232057297A00A300D5 /* MediaPageViewController.swift in Sources */, 452C468F1E427E200087B011 /* OutboundCallInitiator.swift in Sources */, @@ -3915,6 +3946,7 @@ B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */, 3488F9362191CC4000E524CC /* ConversationMediaView.swift in Sources */, 45F32C242057297A00A300D5 /* MessageDetailViewController.swift in Sources */, + C396DAEF2518408B00FF6DC5 /* ParsingState.swift in Sources */, 3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */, B8BB82B523947F2D00BA5194 /* TextField.swift in Sources */, 34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */, diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/CSV.swift b/Signal/src/Loki/Dependencies/SwiftCSV/CSV.swift new file mode 100644 index 000000000..c286ac2a3 --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/CSV.swift @@ -0,0 +1,133 @@ +// +// CSV.swift +// SwiftCSV +// +// Created by Naoto Kaneko on 2/18/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import Foundation + +public protocol View { + associatedtype Rows + associatedtype Columns + + var rows: Rows { get } + var columns: Columns { get } + + init(header: [String], text: String, delimiter: Character, limitTo: Int?, loadColumns: Bool) throws +} + +open class CSV { + static public let comma: Character = "," + + public let header: [String] + + lazy var _namedView: NamedView = { + return try! NamedView( + header: self.header, + text: self.text, + delimiter: self.delimiter, + loadColumns: self.loadColumns) + }() + + lazy var _enumeratedView: EnumeratedView = { + return try! EnumeratedView( + header: self.header, + text: self.text, + delimiter: self.delimiter, + loadColumns: self.loadColumns) + }() + + var text: String + var delimiter: Character + + let loadColumns: Bool + + /// List of dictionaries that contains the CSV data + public var namedRows: [[String : String]] { + return _namedView.rows + } + + /// Dictionary of header name to list of values in that column + /// Will not be loaded if loadColumns in init is false + public var namedColumns: [String : [String]] { + return _namedView.columns + } + + /// Collection of column fields that contain the CSV data + public var enumeratedRows: [[String]] { + return _enumeratedView.rows + } + + /// Collection of columns with metadata. + /// Will not be loaded if loadColumns in init is false + public var enumeratedColumns: [EnumeratedView.Column] { + return _enumeratedView.columns + } + + + @available(*, unavailable, renamed: "namedRows") + public var rows: [[String : String]] { + return namedRows + } + + @available(*, unavailable, renamed: "namedColumns") + public var columns: [String : [String]] { + return namedColumns + } + + + /// Load CSV data from a string. + /// + /// - parameter string: CSV contents to parse. + /// - parameter delimiter: Character used to separate row and header fields (default is ',') + /// - parameter loadColumns: Whether to populate the `columns` dictionary (default is `true`) + /// - throws: `CSVParseError` when parsing `string` fails. + public init(string: String, delimiter: Character = comma, loadColumns: Bool = true) throws { + self.text = string + self.delimiter = delimiter + self.loadColumns = loadColumns + self.header = try Parser.array(text: string, delimiter: delimiter, limitTo: 1).first ?? [] + } + + @available(*, deprecated, message: "Use init(url:delimiter:encoding:loadColumns:) instead of this path-based approach. Also, calling the parameter `name` instead of `path` was a mistake.") + public convenience init(name: String, delimiter: Character = comma, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + try self.init(url: URL(fileURLWithPath: name), delimiter: delimiter, encoding: encoding, loadColumns: loadColumns) + } + + /// Load a CSV file as a named resource from `bundle`. + /// + /// - parameter name: Name of the file resource inside `bundle`. + /// - parameter ext: File extension of the resource; use `nil` to load the first file matching the name (default is `nil`) + /// - parameter bundle: `Bundle` to use for resource lookup (default is `.main`) + /// - parameter delimiter: Character used to separate row and header fields (default is ',') + /// - parameter encoding: encoding used to read file (default is `.utf8`) + /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - throws: `CSVParseError` when parsing the contents of the resource fails, or file loading errors. + /// - returns: `nil` if the resource could not be found + public convenience init?(name: String, extension ext: String? = nil, bundle: Bundle = .main, delimiter: Character = comma, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + guard let url = bundle.url(forResource: name, withExtension: ext) else { + return nil + } + try self.init(url: url, delimiter: delimiter, encoding: encoding, loadColumns: loadColumns) + } + + /// Load a CSV file from `url`. + /// + /// - parameter url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) + /// - parameter delimiter: Character used to separate row and header fields (default is ',') + /// - parameter encoding: Character encoding to read file (default is `.utf8`) + /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. + public convenience init(url: URL, delimiter: Character = comma, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + let contents = try String(contentsOf: url, encoding: encoding) + + try self.init(string: contents, delimiter: delimiter, loadColumns: loadColumns) + } + + /// Turn the CSV data into NSData using a given encoding + open func dataUsingEncoding(_ encoding: String.Encoding) -> Data? { + return description.data(using: encoding) + } +} diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/Description.swift b/Signal/src/Loki/Dependencies/SwiftCSV/Description.swift new file mode 100644 index 000000000..d5c1d7cb1 --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/Description.swift @@ -0,0 +1,30 @@ +// +// Description.swift +// SwiftCSV +// +// Created by Will Richardson on 11/04/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import Foundation + +extension CSV: CustomStringConvertible { + public var description: String { + let head = header.joined(separator: ",") + "\n" + let cont = namedRows.map { row in + return header.map { key -> String in + let value = row[key]! + + // Add quotes if value contains a comma + if value.contains(",") { + return "\"\(value)\"" + } + return value + + }.joined(separator: ",") + + }.joined(separator: "\n") + return head + cont + } +} + diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/EnumeratedView.swift b/Signal/src/Loki/Dependencies/SwiftCSV/EnumeratedView.swift new file mode 100644 index 000000000..5c78cfac4 --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/EnumeratedView.swift @@ -0,0 +1,43 @@ +// +// EnumeratedView.swift +// SwiftCSV +// +// Created by Christian Tietze on 25/10/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import Foundation + +public struct EnumeratedView: View { + + public struct Column { + public let header: String + public let rows: [String] + } + + public private(set) var rows: [[String]] + public private(set) var columns: [Column] + + public init(header: [String], text: String, delimiter: Character, limitTo: Int? = nil, loadColumns: Bool = false) throws { + + var rows = [[String]]() + var columns: [EnumeratedView.Column] = [] + + try Parser.enumerateAsArray(text: text, delimiter: delimiter, limitTo: limitTo, startAt: 1) { fields in + rows.append(fields) + } + + if loadColumns { + columns = header.enumerated().map { (index: Int, header: String) -> EnumeratedView.Column in + + return EnumeratedView.Column( + header: header, + rows: rows.map { $0[index] }) + } + } + + self.rows = rows + self.columns = columns + } + +} diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/NamedView.swift b/Signal/src/Loki/Dependencies/SwiftCSV/NamedView.swift new file mode 100644 index 000000000..c18c1a50c --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/NamedView.swift @@ -0,0 +1,32 @@ +// +// NamedView.swift +// SwiftCSV +// +// Created by Christian Tietze on 22/10/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +public struct NamedView: View { + + public var rows: [[String : String]] + public var columns: [String : [String]] + + public init(header: [String], text: String, delimiter: Character, limitTo: Int? = nil, loadColumns: Bool = false) throws { + + var rows = [[String: String]]() + var columns = [String: [String]]() + + try Parser.enumerateAsDict(header: header, content: text, delimiter: delimiter, limitTo: limitTo) { dict in + rows.append(dict) + } + + if loadColumns { + for field in header { + columns[field] = rows.map { $0[field] ?? "" } + } + } + + self.rows = rows + self.columns = columns + } +} diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/Parser.swift b/Signal/src/Loki/Dependencies/SwiftCSV/Parser.swift new file mode 100644 index 000000000..ee05501e0 --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/Parser.swift @@ -0,0 +1,111 @@ +// +// Parser.swift +// SwiftCSV +// +// Created by Will Richardson on 13/04/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +extension CSV { + /// Parse the file and call a block on each row, passing it in as a list of fields + /// limitTo will limit the result to a certain number of lines + public func enumerateAsArray(limitTo: Int? = nil, startAt: Int = 0, _ block: @escaping ([String]) -> ()) throws { + + try Parser.enumerateAsArray(text: self.text, delimiter: self.delimiter, limitTo: limitTo, startAt: startAt, block: block) + } + + public func enumerateAsDict(_ block: @escaping ([String : String]) -> ()) throws { + + try Parser.enumerateAsDict(header: self.header, content: self.text, delimiter: self.delimiter, block: block) + } +} + +enum Parser { + + static func array(text: String, delimiter: Character, limitTo: Int? = nil, startAt: Int = 0) throws -> [[String]] { + + var rows = [[String]]() + + try enumerateAsArray(text: text, delimiter: delimiter) { row in + rows.append(row) + } + + return rows + } + + /// Parse the text and call a block on each row, passing it in as a list of fields. + /// + /// - parameter text: Text to parse. + /// - parameter delimiter: Character to split row and header fields by (default is ',') + /// - parameter limitTo: If set to non-nil value, enumeration stops + /// at the row with index `limitTo` (or on end-of-text, whichever is earlier. + /// - parameter startAt: Offset of rows to ignore before invoking `block` for the first time. Default is 0. + /// - parameter block: Callback invoked for every parsed row between `startAt` and `limitTo` in `text`. + static func enumerateAsArray(text: String, delimiter: Character, limitTo: Int? = nil, startAt: Int = 0, block: @escaping ([String]) -> ()) throws { + var currentIndex = text.startIndex + let endIndex = text.endIndex + + var fields = [String]() + var field = "" + + var count = 0 + + func finishRow() { + fields.append(String(field)) + if count >= startAt { + block(fields) + } + count += 1 + fields = [String]() + field = "" + } + + var state: ParsingState = ParsingState( + delimiter: delimiter, + finishRow: finishRow, + appendChar: { field.append($0) }, + finishField: { + fields.append(field) + field = "" + }) + + func limitReached(_ count: Int) -> Bool { + + guard let limitTo = limitTo, + count >= limitTo + else { return false } + + return true + } + + while currentIndex < endIndex { + let char = text[currentIndex] + + try state.change(char) + + if limitReached(count) { + break + } + + currentIndex = text.index(after: currentIndex) + } + + if !fields.isEmpty || !field.isEmpty || limitReached(count) { + fields.append(field) + block(fields) + } + } + + static func enumerateAsDict(header: [String], content: String, delimiter: Character, limitTo: Int? = nil, block: @escaping ([String : String]) -> ()) throws { + + let enumeratedHeader = header.enumerated() + + try enumerateAsArray(text: content, delimiter: delimiter, startAt: 1) { fields in + var dict = [String: String]() + for (index, head) in enumeratedHeader { + dict[head] = index < fields.count ? fields[index] : "" + } + block(dict) + } + } +} diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/ParsingState.swift b/Signal/src/Loki/Dependencies/SwiftCSV/ParsingState.swift new file mode 100644 index 000000000..8aee7a06d --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/ParsingState.swift @@ -0,0 +1,106 @@ +// +// ParsingState.swift +// SwiftCSV +// +// Created by Christian Tietze on 25/10/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +public enum CSVParseError: Error { + case generic(message: String) + case quotation(message: String) +} + +/// State machine of parsing CSV contents character by character. +struct ParsingState { + + private(set) var atStart = true + private(set) var parsingField = false + private(set) var parsingQuotes = false + private(set) var innerQuotes = false + + let delimiter: Character + let finishRow: () -> Void + let appendChar: (Character) -> Void + let finishField: () -> Void + + init(delimiter: Character, + finishRow: @escaping () -> Void, + appendChar: @escaping (Character) -> Void, + finishField: @escaping () -> Void) { + + self.delimiter = delimiter + self.finishRow = finishRow + self.appendChar = appendChar + self.finishField = finishField + } + + mutating func change(_ char: Character) throws { + if atStart { + if char == "\"" { + atStart = false + parsingQuotes = true + } else if char == delimiter { + finishField() + } else if char.isNewline { + finishRow() + } else { + parsingField = true + atStart = false + appendChar(char) + } + } else if parsingField { + if innerQuotes { + if char == "\"" { + appendChar(char) + innerQuotes = false + } else { + throw CSVParseError.quotation(message: "Can't have non-quote here: \(char)") + } + } else { + if char == "\"" { + innerQuotes = true + } else if char == delimiter { + atStart = true + parsingField = false + innerQuotes = false + finishField() + } else if char.isNewline { + atStart = true + parsingField = false + innerQuotes = false + finishRow() + } else { + appendChar(char) + } + } + } else if parsingQuotes { + if innerQuotes { + if char == "\"" { + appendChar(char) + innerQuotes = false + } else if char == delimiter { + atStart = true + parsingField = false + innerQuotes = false + finishField() + } else if char.isNewline { + atStart = true + parsingQuotes = false + innerQuotes = false + finishRow() + } else { + throw CSVParseError.quotation(message: "Can't have non-quote here: \(char)") + } + } else { + if char == "\"" { + innerQuotes = true + } else { + appendChar(char) + } + } + } else { + throw CSVParseError.generic(message: "me_irl") + } + } +} diff --git a/Signal/src/Loki/Dependencies/SwiftCSV/String+Lines.swift b/Signal/src/Loki/Dependencies/SwiftCSV/String+Lines.swift new file mode 100644 index 000000000..d824d3f0f --- /dev/null +++ b/Signal/src/Loki/Dependencies/SwiftCSV/String+Lines.swift @@ -0,0 +1,23 @@ +// +// String+Lines.swift +// SwiftCSV +// +// Created by Naoto Kaneko on 2/24/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +extension String { + internal var firstLine: String { + var current = startIndex + while current < endIndex && self[current].isNewline == false { + current = self.index(after: current) + } + return String(self[..