mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
70
mobile/ios/Shared/Extensions/ArrayExtensions.swift
Normal file
70
mobile/ios/Shared/Extensions/ArrayExtensions.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array where Element: Comparable {
|
||||
func sameElements(_ arr: [Element]) -> Bool {
|
||||
guard self.count == arr.count else { return false }
|
||||
let sorted = self.sorted(by: <)
|
||||
let arrSorted = arr.sorted(by: <)
|
||||
for elements in sorted.zip(arrSorted) where elements.0 != elements.1 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array {
|
||||
|
||||
func find(_ f: (Iterator.Element) -> Bool) -> Iterator.Element? {
|
||||
for x in self {
|
||||
if f(x) {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(_ x: Element, f: (Element, Element) -> Bool) -> Bool {
|
||||
for y in self {
|
||||
if f(x, y) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Performs a union operator using the result of f(Element) as the value to base uniqueness on.
|
||||
func union<T: Hashable>(_ arr: [Element], f: ((Element) -> T)) -> [Element] {
|
||||
let result = self + arr
|
||||
return result.unique(f)
|
||||
}
|
||||
|
||||
// Returns unique values in an array using the result of f()
|
||||
func unique<T: Hashable>(_ f: ((Element) -> T)) -> [Element] {
|
||||
var map: [T: Element] = [T: Element]()
|
||||
return self.flatMap { a in
|
||||
let t = f(a)
|
||||
if map[t] == nil {
|
||||
map[t] = a
|
||||
return a
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension Sequence {
|
||||
func every(_ f: (Self.Iterator.Element) -> Bool) -> Bool {
|
||||
for x in self {
|
||||
if !f(x) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
19
mobile/ios/Shared/Extensions/DataExtensions.swift
Normal file
19
mobile/ios/Shared/Extensions/DataExtensions.swift
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Data {
|
||||
public mutating func appendBytes(fromData data: Data) {
|
||||
var bytes = [UInt8](repeating: 0, count: data.count)
|
||||
data.copyBytes(to: &bytes, count: data.count)
|
||||
self.append(bytes, count: bytes.count)
|
||||
}
|
||||
|
||||
public func getBytes() -> [UInt8] {
|
||||
var bytes = [UInt8](repeating: 0, count: self.count)
|
||||
self.copyBytes(to: &bytes, count: self.count)
|
||||
return bytes
|
||||
}
|
||||
}
|
||||
9
mobile/ios/Shared/Extensions/DictionaryExtensions.swift
Normal file
9
mobile/ios/Shared/Extensions/DictionaryExtensions.swift
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
extension Dictionary {
|
||||
public mutating func merge(with dictionary: Dictionary) {
|
||||
dictionary.forEach { updateValue($1, forKey: $0) }
|
||||
}
|
||||
}
|
||||
74
mobile/ios/Shared/Extensions/HashExtensions.swift
Normal file
74
mobile/ios/Shared/Extensions/HashExtensions.swift
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
public var sha1: Data {
|
||||
let len = Int(CC_SHA1_DIGEST_LENGTH)
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CC_SHA1((self as NSData).bytes, CC_LONG(self.count), digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
|
||||
public var sha256: Data {
|
||||
let len = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CC_SHA256((self as NSData).bytes, CC_LONG(self.count), digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
public var sha1: Data {
|
||||
let data = self.data(using: String.Encoding.utf8)!
|
||||
return data.sha1
|
||||
}
|
||||
|
||||
public var sha256: Data {
|
||||
let data = self.data(using: String.Encoding.utf8)!
|
||||
return data.sha256
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public func hmacSha256WithKey(_ key: Data) -> Data {
|
||||
let len = Int(CC_SHA256_DIGEST_LENGTH)
|
||||
|
||||
let digest = UnsafeMutablePointer<UInt8>.allocate(capacity: len)
|
||||
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256),
|
||||
(key as NSData).bytes, Int(key.count),
|
||||
(self as NSData).bytes, Int(self.count),
|
||||
digest)
|
||||
return Data(bytes: UnsafePointer<UInt8>(digest), count: len)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
public var utf8EncodedData: Data {
|
||||
return self.data(using: String.Encoding.utf8, allowLossyConversion: false)!
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public var utf8EncodedString: String? {
|
||||
return NSString(data: self, encoding: String.Encoding.utf8.rawValue) as String?
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public func xoredWith(_ other: Data) -> Data? {
|
||||
if self.count != other.count {
|
||||
return nil
|
||||
}
|
||||
var xoredBytes = [UInt8](repeating: 0, count: self.count)
|
||||
let selfBytes = (self as NSData).bytes.bindMemory(to: UInt8.self, capacity: self.count)
|
||||
let otherBytes = (other as NSData).bytes.bindMemory(to: UInt8.self, capacity: other.count)
|
||||
for i in 0..<self.count {
|
||||
xoredBytes[i] = selfBytes[i] ^ otherBytes[i]
|
||||
}
|
||||
return Data(bytes: UnsafePointer<UInt8>(xoredBytes), count: self.count)
|
||||
}
|
||||
|
||||
}
|
||||
70
mobile/ios/Shared/Extensions/HexExtensions.swift
Normal file
70
mobile/ios/Shared/Extensions/HexExtensions.swift
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public var hexDecodedData: Data {
|
||||
// Convert to a CString and make sure it has an even number of characters (terminating 0 is included, so we
|
||||
// check for uneven!)
|
||||
guard let cString = self.cString(using: String.Encoding.ascii), (cString.count % 2) == 1 else {
|
||||
return Data()
|
||||
}
|
||||
|
||||
var result = Data(capacity: (cString.count - 1) / 2)
|
||||
for i in stride(from: 0, to: (cString.count - 1), by: 2) {
|
||||
guard let l = hexCharToByte(cString[i]), let r = hexCharToByte(cString[i+1]) else {
|
||||
return Data()
|
||||
}
|
||||
var value: UInt8 = (l << 4) | r
|
||||
result.append(&value, count: MemoryLayout.size(ofValue: value))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private func hexCharToByte(_ c: CChar) -> UInt8? {
|
||||
if c >= 48 && c <= 57 { // 0 - 9
|
||||
return UInt8(c - 48)
|
||||
}
|
||||
if c >= 97 && c <= 102 { // a - f
|
||||
return UInt8(10) + UInt8(c - 97)
|
||||
}
|
||||
if c >= 65 && c <= 70 { // A - F
|
||||
return UInt8(10) + UInt8(c - 65)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private let HexDigits: [String] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"]
|
||||
|
||||
extension Data {
|
||||
public var hexEncodedString: String {
|
||||
var result = String()
|
||||
result.reserveCapacity(count * 2)
|
||||
withUnsafeBytes { (p: UnsafePointer<UInt8>) in
|
||||
for i in 0..<count {
|
||||
result.append(HexDigits[Int((p[i] & 0xf0) >> 4)])
|
||||
result.append(HexDigits[Int(p[i] & 0x0f)])
|
||||
}
|
||||
}
|
||||
return String(result)
|
||||
}
|
||||
|
||||
public static func randomOfLength(_ length: UInt) -> Data? {
|
||||
let length = Int(length)
|
||||
var data = Data(count: length)
|
||||
var result: Int32 = 0
|
||||
data.withUnsafeMutableBytes { (p: UnsafeMutablePointer<UInt8>) in
|
||||
result = SecRandomCopyBytes(kSecRandomDefault, length, p)
|
||||
}
|
||||
return result == 0 ? data : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
public var base64EncodedString: String {
|
||||
return self.base64EncodedString(options: NSData.Base64EncodingOptions())
|
||||
}
|
||||
}
|
||||
65
mobile/ios/Shared/Extensions/JSONExtensions.swift
Normal file
65
mobile/ios/Shared/Extensions/JSONExtensions.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
public extension JSON {
|
||||
func isStringOrNull() -> Bool {
|
||||
return self.isString() ||
|
||||
self.isNull()
|
||||
}
|
||||
|
||||
func isError() -> Bool {
|
||||
return self.error != nil
|
||||
}
|
||||
|
||||
func isString() -> Bool {
|
||||
// SwiftyJSON doesn't link values to types; it's possible for `self.type == .string` but
|
||||
// `self.string` to return `nil`. Validate both.
|
||||
return self.type == .string &&
|
||||
self.string != nil
|
||||
}
|
||||
|
||||
func isBool() -> Bool {
|
||||
return self.type == .bool
|
||||
}
|
||||
|
||||
func isArray() -> Bool {
|
||||
return self.type == .array
|
||||
}
|
||||
|
||||
func isDictionary() -> Bool {
|
||||
return self.type == .dictionary
|
||||
}
|
||||
|
||||
// Bear in mind that for this function to work you need to set the value to NSNull:
|
||||
// ```
|
||||
// var myObj = JSON(…)
|
||||
// myObj["foo"] = someOptional ?? NSNull()
|
||||
// ```
|
||||
// This is… easy to get wrong.
|
||||
func isNull() -> Bool {
|
||||
return self.type == .null
|
||||
}
|
||||
|
||||
func isInt() -> Bool {
|
||||
return self.type == .number && self.int != nil
|
||||
}
|
||||
|
||||
func isNumber() -> Bool {
|
||||
return self.type == .number && self.number != nil
|
||||
}
|
||||
|
||||
func isDouble() -> Bool {
|
||||
return self.type == .number && self.double != nil
|
||||
}
|
||||
|
||||
// SwiftyJSON pretty prints the string value by default. Since all of our
|
||||
// existing code required the string to not be pretty printed, this helper
|
||||
// can be used as a shorthand for non-pretty printed strings.
|
||||
func stringValue() -> String? {
|
||||
return self.rawString(.utf8, options: [])
|
||||
}
|
||||
}
|
||||
62
mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift
Normal file
62
mobile/ios/Shared/Extensions/KeychainWrapperExtensions.swift
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import XCGLogger
|
||||
import SwiftKeychainWrapper
|
||||
|
||||
private let log = Logger.keychainLogger
|
||||
|
||||
public extension KeychainWrapper {
|
||||
static var sharedAppContainerKeychain: KeychainWrapper {
|
||||
let baseBundleIdentifier = AppInfo.baseBundleIdentifier
|
||||
let accessGroupPrefix = Bundle.main.object(forInfoDictionaryKey: "MozDevelopmentTeam") as! String
|
||||
let accessGroupIdentifier = AppInfo.keychainAccessGroupWithPrefix(accessGroupPrefix)
|
||||
return KeychainWrapper(serviceName: baseBundleIdentifier, accessGroup: accessGroupIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
public extension KeychainWrapper {
|
||||
func ensureStringItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
|
||||
if self.hasValue(forKey: key) {
|
||||
if self.accessibilityOfKey(key) != .afterFirstUnlock {
|
||||
log.debug("updating item \(key) with \(accessibility)")
|
||||
|
||||
guard let value = self.string(forKey: key) else {
|
||||
log.error("failed to get item \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
if !self.removeObject(forKey: key) {
|
||||
log.warning("failed to remove item \(key)")
|
||||
}
|
||||
|
||||
if !self.set(value, forKey: key, withAccessibility: accessibility) {
|
||||
log.warning("failed to update item \(key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ensureObjectItemAccessibility(_ accessibility: SwiftKeychainWrapper.KeychainItemAccessibility, forKey key: String) {
|
||||
if self.hasValue(forKey: key) {
|
||||
if self.accessibilityOfKey(key) != .afterFirstUnlock {
|
||||
log.debug("updating item \(key) with \(accessibility)")
|
||||
|
||||
guard let value = self.object(forKey: key) else {
|
||||
log.error("failed to get item \(key)")
|
||||
return
|
||||
}
|
||||
|
||||
if !self.removeObject(forKey: key) {
|
||||
log.warning("failed to remove item \(key)")
|
||||
}
|
||||
|
||||
if !self.set(value, forKey: key, withAccessibility: accessibility) {
|
||||
log.warning("failed to update item \(key)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift
Normal file
15
mobile/ios/Shared/Extensions/NSCharacterSetExtensions.swift
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CharacterSet {
|
||||
public static func URLAllowedCharacterSet() -> CharacterSet {
|
||||
return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%")
|
||||
}
|
||||
|
||||
public static func SearchTermsAllowedCharacterSet() -> CharacterSet {
|
||||
return CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-_.")
|
||||
}
|
||||
}
|
||||
41
mobile/ios/Shared/Extensions/NSCoderExtensions.swift
Normal file
41
mobile/ios/Shared/Extensions/NSCoderExtensions.swift
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* There are some oddnesses around the different ways that NSKeyedArchiver decodes objects based on whether or not they were
|
||||
* originally encoded using Swift 2.x or Swift 3.
|
||||
* If the object was encoded on Swift 2.x, then you need to use decodeObject to unwrap it. But that will return a nil if the object was encoded on Swift 3
|
||||
* For swift 3 encoded objects to you need to use decode<Type>
|
||||
* These helper functions provide a unified way of achieving that
|
||||
**/
|
||||
extension NSCoder {
|
||||
/**
|
||||
* Decode as Int regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsInt(forKey key: String) -> Int {
|
||||
return self.decodeObject(forKey: key) as? Int ?? self.decodeInteger(forKey: key)
|
||||
}
|
||||
/**
|
||||
* Decode as UInt64 regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsUInt64(forKey key: String) -> UInt64 {
|
||||
return (self.decodeObject(forKey: key) as? NSNumber)?.uint64Value ?? UInt64(self.decodeInt64(forKey: key))
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode as Bool regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsBool(forKey key: String) -> Bool {
|
||||
return self.decodeObject(forKey: key) as? Bool ?? self.decodeBool(forKey: key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode as Double regardless of which Swift version was used to encode it
|
||||
**/
|
||||
open func decodeAsDouble(forKey key: String) -> Double {
|
||||
return (self.decodeObject(forKey: key) as? NSNumber)?.doubleValue ?? self.decodeDouble(forKey: key)
|
||||
}
|
||||
}
|
||||
112
mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift
Normal file
112
mobile/ios/Shared/Extensions/NSFileManagerExtensions.swift
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* Created and contributed by Nikolai Ruhe and rewritten in Swift.
|
||||
* https://github.com/NikolaiRuhe/NRFoundation */
|
||||
|
||||
import Foundation
|
||||
|
||||
public let NSFileManagerExtensionsDomain = "org.mozilla.NSFileManagerExtensions"
|
||||
|
||||
public enum NSFileManagerExtensionsErrorCodes: Int {
|
||||
case enumeratorFailure = 0
|
||||
case enumeratorElementNotURL = 1
|
||||
case errorEnumeratingDirectory = 2
|
||||
}
|
||||
|
||||
public extension FileManager {
|
||||
|
||||
private func directoryEnumeratorForURL(_ url: URL) throws -> FileManager.DirectoryEnumerator {
|
||||
let prefetchedProperties = [
|
||||
URLResourceKey.isRegularFileKey,
|
||||
URLResourceKey.fileAllocatedSizeKey,
|
||||
URLResourceKey.totalFileAllocatedSizeKey
|
||||
]
|
||||
|
||||
// If we run into an issue getting an enumerator for the given URL, capture the error and bail out later.
|
||||
var enumeratorError: NSError?
|
||||
let errorHandler: (URL, Error) -> Bool = { _, error in
|
||||
enumeratorError = error as NSError
|
||||
return false
|
||||
}
|
||||
|
||||
guard let directoryEnumerator = FileManager.default.enumerator(at: url,
|
||||
includingPropertiesForKeys: prefetchedProperties,
|
||||
options: [],
|
||||
errorHandler: errorHandler) else {
|
||||
throw errorWithCode(.enumeratorFailure)
|
||||
}
|
||||
|
||||
// Bail out if we encountered an issue getting the enumerator.
|
||||
if let _ = enumeratorError {
|
||||
throw errorWithCode(.errorEnumeratingDirectory, underlyingError: enumeratorError)
|
||||
}
|
||||
|
||||
return directoryEnumerator
|
||||
}
|
||||
|
||||
private func sizeForItemURL(_ url: Any, withPrefix prefix: String) throws -> Int64 {
|
||||
guard let itemURL = url as? URL else {
|
||||
throw errorWithCode(.enumeratorElementNotURL)
|
||||
}
|
||||
|
||||
// Skip files that are not regular and don't match our prefix
|
||||
guard itemURL.isRegularFile && itemURL.lastComponentIsPrefixedBy(prefix) else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return itemURL.allocatedFileSize()
|
||||
}
|
||||
|
||||
func allocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String, isLargerThanBytes threshold: Int64) throws -> Bool {
|
||||
let directoryEnumerator = try directoryEnumeratorForURL(url)
|
||||
var acc: Int64 = 0
|
||||
for item in directoryEnumerator {
|
||||
acc += try sizeForItemURL(item as AnyObject, withPrefix: prefix)
|
||||
if acc > threshold {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the precise size of the given directory on disk.
|
||||
|
||||
- parameter url: Directory URL
|
||||
- parameter prefix: Prefix of files to check for size
|
||||
|
||||
- throws: Error reading/operating on disk.
|
||||
*/
|
||||
func getAllocatedSizeOfDirectoryAtURL(_ url: URL, forFilesPrefixedWith prefix: String) throws -> Int64 {
|
||||
let directoryEnumerator = try directoryEnumeratorForURL(url)
|
||||
return try directoryEnumerator.reduce(0) {
|
||||
let size = try sizeForItemURL($1 as AnyObject, withPrefix: prefix)
|
||||
return $0 + size
|
||||
}
|
||||
}
|
||||
|
||||
func contentsOfDirectoryAtPath(_ path: String, withFilenamePrefix prefix: String) throws -> [String] {
|
||||
return try FileManager.default.contentsOfDirectory(atPath: path)
|
||||
.filter { $0.hasPrefix("\(prefix).") }
|
||||
.sorted { $0 < $1 }
|
||||
}
|
||||
|
||||
func removeItemInDirectory(_ directory: String, named: String) throws {
|
||||
let file = URL(fileURLWithPath: directory).appendingPathComponent(named).path
|
||||
try self.removeItem(atPath: file)
|
||||
}
|
||||
|
||||
private func errorWithCode(_ code: NSFileManagerExtensionsErrorCodes, underlyingError error: NSError? = nil) -> NSError {
|
||||
var userInfo = [String: AnyObject]()
|
||||
if let _ = error {
|
||||
userInfo[NSUnderlyingErrorKey] = error
|
||||
}
|
||||
|
||||
return NSError(
|
||||
domain: NSFileManagerExtensionsDomain,
|
||||
code: code.rawValue,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
extension NSMutableAttributedString {
|
||||
public func colorSubstring(_ substring: String, withColor color: UIColor) {
|
||||
self.attributeSubstring(substring, forAttribute: NSForegroundColorAttributeName, withValue: color)
|
||||
}
|
||||
|
||||
public func pitchSubstring(_ substring: String, withPitch pitch: Double) {
|
||||
let pitchValue = NSNumber(value: pitch as Double)
|
||||
self.attributeSubstring(substring, forAttribute: UIAccessibilitySpeechAttributePitch, withValue: pitchValue)
|
||||
}
|
||||
|
||||
private func attributeSubstring(_ substring: String, forAttribute attribute: String, withValue value: AnyObject) {
|
||||
let nsString = self.string as NSString
|
||||
let range = nsString.range(of: substring)
|
||||
self.addAttribute(attribute, value: value, range: range)
|
||||
}
|
||||
}
|
||||
31
mobile/ios/Shared/Extensions/NSScannerExtensions.swift
Normal file
31
mobile/ios/Shared/Extensions/NSScannerExtensions.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Scanner {
|
||||
public func scanUnsignedLongLong() -> UInt64? {
|
||||
var value: UInt64 = 0
|
||||
if scanUnsignedLongLong(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func scanLongLong() -> Int64? {
|
||||
var value: Int64 = 0
|
||||
if scanInt64(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func scanDouble() -> Double? {
|
||||
var value: Double = 0
|
||||
if scanDouble(&value) {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
20
mobile/ios/Shared/Extensions/NSStringExtensions.swift
Normal file
20
mobile/ios/Shared/Extensions/NSStringExtensions.swift
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public static func contentsOfFileWithResourceName(_ name: String, ofType type: String, fromBundle bundle: Bundle, encoding: String.Encoding, error: NSErrorPointer) -> String? {
|
||||
if let path = bundle.path(forResource: name, ofType: type) {
|
||||
do {
|
||||
return try String(contentsOfFile: path, encoding: encoding)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
498
mobile/ios/Shared/Extensions/NSURLExtensions.swift
Normal file
498
mobile/ios/Shared/Extensions/NSURLExtensions.swift
Normal file
|
|
@ -0,0 +1,498 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
private struct ETLDEntry: CustomStringConvertible {
|
||||
let entry: String
|
||||
|
||||
var isNormal: Bool { return isWild || !isException }
|
||||
var isWild: Bool = false
|
||||
var isException: Bool = false
|
||||
|
||||
init(entry: String) {
|
||||
self.entry = entry
|
||||
self.isWild = entry.hasPrefix("*")
|
||||
self.isException = entry.hasPrefix("!")
|
||||
}
|
||||
|
||||
fileprivate var description: String {
|
||||
return "{ Entry: \(entry), isWildcard: \(isWild), isException: \(isException) }"
|
||||
}
|
||||
}
|
||||
|
||||
private typealias TLDEntryMap = [String: ETLDEntry]
|
||||
|
||||
private func loadEntriesFromDisk() -> TLDEntryMap? {
|
||||
if let data = String.contentsOfFileWithResourceName("effective_tld_names", ofType: "dat", fromBundle: Bundle(identifier: "org.mozilla.Shared")!, encoding: String.Encoding.utf8, error: nil) {
|
||||
let lines = data.components(separatedBy: "\n")
|
||||
let trimmedLines = lines.filter { !$0.hasPrefix("//") && $0 != "\n" && $0 != "" }
|
||||
|
||||
var entries = TLDEntryMap()
|
||||
for line in trimmedLines {
|
||||
let entry = ETLDEntry(entry: line)
|
||||
let key: String
|
||||
if entry.isWild {
|
||||
// Trim off the '*.' part of the line
|
||||
key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 2))
|
||||
} else if entry.isException {
|
||||
// Trim off the '!' part of the line
|
||||
key = line.substring(from: line.characters.index(line.startIndex, offsetBy: 1))
|
||||
} else {
|
||||
key = line
|
||||
}
|
||||
entries[key] = entry
|
||||
}
|
||||
return entries
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private var etldEntries: TLDEntryMap? = {
|
||||
return loadEntriesFromDisk()
|
||||
}()
|
||||
|
||||
// MARK: - Local Resource URL Extensions
|
||||
extension URL {
|
||||
|
||||
public func allocatedFileSize() -> Int64 {
|
||||
// First try to get the total allocated size and in failing that, get the file allocated size
|
||||
return getResourceLongLongForKey(URLResourceKey.totalFileAllocatedSizeKey.rawValue)
|
||||
?? getResourceLongLongForKey(URLResourceKey.fileAllocatedSizeKey.rawValue)
|
||||
?? 0
|
||||
}
|
||||
|
||||
public func getResourceValueForKey(_ key: String) -> Any? {
|
||||
let resourceKey = URLResourceKey(key)
|
||||
let keySet = Set<URLResourceKey>([resourceKey])
|
||||
|
||||
var val: Any?
|
||||
do {
|
||||
let values = try resourceValues(forKeys: keySet)
|
||||
val = values.allValues[resourceKey]
|
||||
} catch _ {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
public func getResourceLongLongForKey(_ key: String) -> Int64? {
|
||||
return (getResourceValueForKey(key) as? NSNumber)?.int64Value
|
||||
}
|
||||
|
||||
public func getResourceBoolForKey(_ key: String) -> Bool? {
|
||||
return getResourceValueForKey(key) as? Bool
|
||||
}
|
||||
|
||||
public var isRegularFile: Bool {
|
||||
return getResourceBoolForKey(URLResourceKey.isRegularFileKey.rawValue) ?? false
|
||||
}
|
||||
|
||||
public func lastComponentIsPrefixedBy(_ prefix: String) -> Bool {
|
||||
return (pathComponents.last?.hasPrefix(prefix) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
// The list of permanent URI schemes has been taken from http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
|
||||
private let permanentURISchemes = ["aaa", "aaas", "about", "acap", "acct", "cap", "cid", "coap", "coaps", "crid", "data", "dav", "dict", "dns", "example", "file", "ftp", "geo", "go", "gopher", "h323", "http", "https", "iax", "icap", "im", "imap", "info", "ipp", "ipps", "iris", "iris.beep", "iris.lwz", "iris.xpc", "iris.xpcs", "jabber", "ldap", "mailto", "mid", "msrp", "msrps", "mtqp", "mupdate", "news", "nfs", "ni", "nih", "nntp", "opaquelocktoken", "pkcs11", "pop", "pres", "reload", "rtsp", "rtsps", "rtspu", "service", "session", "shttp", "sieve", "sip", "sips", "sms", "snmp", "soap.beep", "soap.beeps", "stun", "stuns", "tag", "tel", "telnet", "tftp", "thismessage", "tip", "tn3270", "turn", "turns", "tv", "urn", "vemmi", "vnc", "ws", "wss", "xcon", "xcon-userid", "xmlrpc.beep", "xmlrpc.beeps", "xmpp", "z39.50r", "z39.50s"]
|
||||
|
||||
extension URL {
|
||||
|
||||
public func withQueryParams(_ params: [URLQueryItem]) -> URL {
|
||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||
var items = (components.queryItems ?? [])
|
||||
for param in params {
|
||||
items.append(param)
|
||||
}
|
||||
components.queryItems = items
|
||||
return components.url!
|
||||
}
|
||||
|
||||
public func withQueryParam(_ name: String, value: String) -> URL {
|
||||
var components = URLComponents(url: self, resolvingAgainstBaseURL: false)!
|
||||
let item = URLQueryItem(name: name, value: value)
|
||||
components.queryItems = (components.queryItems ?? []) + [item]
|
||||
return components.url!
|
||||
}
|
||||
|
||||
public func getQuery() -> [String: String] {
|
||||
var results = [String: String]()
|
||||
let keyValues = self.query?.components(separatedBy: "&")
|
||||
|
||||
if keyValues?.count ?? 0 > 0 {
|
||||
for pair in keyValues! {
|
||||
let kv = pair.components(separatedBy: "=")
|
||||
if kv.count > 1 {
|
||||
results[kv[0]] = kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
public var hostPort: String? {
|
||||
if let host = self.host {
|
||||
if let port = (self as NSURL).port?.int32Value {
|
||||
return "\(host):\(port)"
|
||||
}
|
||||
return host
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var origin: String? {
|
||||
guard isWebPage(includeDataURIs: false), let hostPort = self.hostPort, let scheme = scheme else {
|
||||
return nil
|
||||
}
|
||||
return "\(scheme)://\(hostPort)"
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the second level domain (SLD) of a url. It removes any subdomain/TLD
|
||||
*
|
||||
* E.g., https://m.foo.com/bar/baz?noo=abc#123 => foo
|
||||
**/
|
||||
public var hostSLD: String {
|
||||
guard let publicSuffix = self.publicSuffix, let baseDomain = self.baseDomain else {
|
||||
return self.normalizedHost ?? self.absoluteString
|
||||
}
|
||||
return baseDomain.replacingOccurrences(of: ".\(publicSuffix)", with: "")
|
||||
}
|
||||
|
||||
public var normalizedHostAndPath: String? {
|
||||
if let normalizedHost = self.normalizedHost {
|
||||
return normalizedHost + self.path
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public var absoluteDisplayString: String {
|
||||
var urlString = self.absoluteString
|
||||
// For http URLs, get rid of the trailing slash if the path is empty or '/'
|
||||
if (self.scheme == "http" || self.scheme == "https") && (self.path == "/") && urlString.endsWith("/") {
|
||||
urlString = urlString.substring(to: urlString.characters.index(urlString.endIndex, offsetBy: -1))
|
||||
}
|
||||
// If it's basic http, strip out the string but leave anything else in
|
||||
if urlString.hasPrefix("http://") {
|
||||
return urlString.substring(from: urlString.characters.index(urlString.startIndex, offsetBy: 7))
|
||||
} else {
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
|
||||
/// String suitable for displaying outside of the app, for example in notifications, were Data Detectors will
|
||||
/// linkify the text and make it into a openable-in-Safari link.
|
||||
public var absoluteDisplayExternalString: String {
|
||||
return self.absoluteDisplayString.replacingOccurrences(of: ".", with: "\u{2024}")
|
||||
}
|
||||
|
||||
public var displayURL: URL? {
|
||||
if self.isReaderModeURL {
|
||||
return self.decodeReaderModeURL?.havingRemovedAuthorisationComponents()
|
||||
}
|
||||
|
||||
if self.isErrorPageURL {
|
||||
if let decodedURL = self.originalURLFromErrorURL {
|
||||
return decodedURL.displayURL
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !self.isAboutURL {
|
||||
return self.havingRemovedAuthorisationComponents()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the base domain from a given hostname. The base domain name is defined as the public domain suffix
|
||||
with the base private domain attached to the front. For example, for the URL www.bbc.co.uk, the base domain
|
||||
would be bbc.co.uk. The base domain includes the public suffix (co.uk) + one level down (bbc).
|
||||
|
||||
:returns: The base domain string for the given host name.
|
||||
*/
|
||||
public var baseDomain: String? {
|
||||
guard !isIPv6, let host = host else { return nil }
|
||||
|
||||
// If this is just a hostname and not a FQDN, use the entire hostname.
|
||||
if !host.contains(".") {
|
||||
return host
|
||||
}
|
||||
|
||||
return publicSuffixFromHost(host, withAdditionalParts: 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns just the domain, but with the same scheme, and a trailing '/'.
|
||||
*
|
||||
* E.g., https://m.foo.com/bar/baz?noo=abc#123 => https://foo.com/
|
||||
*
|
||||
* Any failure? Return this URL.
|
||||
*/
|
||||
public var domainURL: URL {
|
||||
if let normalized = self.normalizedHost {
|
||||
// Use NSURLComponents instead of NSURL since the former correctly preserves
|
||||
// brackets for IPv6 hosts, whereas the latter escapes them.
|
||||
var components = URLComponents()
|
||||
components.scheme = self.scheme
|
||||
components.host = normalized
|
||||
components.path = "/"
|
||||
return components.url ?? self
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
public var normalizedHost: String? {
|
||||
// Use components.host instead of self.host since the former correctly preserves
|
||||
// brackets for IPv6 hosts, whereas the latter strips them.
|
||||
guard let components = URLComponents(url: self, resolvingAgainstBaseURL: false), var host = components.host, host != "" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let range = host.range(of: "^(www|mobile|m)\\.", options: .regularExpression) {
|
||||
host.replaceSubrange(range, with: "")
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
/**
|
||||
Returns the public portion of the host name determined by the public suffix list found here: https://publicsuffix.org/list/.
|
||||
For example for the url www.bbc.co.uk, based on the entries in the TLD list, the public suffix would return co.uk.
|
||||
|
||||
:returns: The public suffix for within the given hostname.
|
||||
*/
|
||||
public var publicSuffix: String? {
|
||||
if let host = self.host {
|
||||
return publicSuffixFromHost(host, withAdditionalParts: 0)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func isWebPage(includeDataURIs: Bool = true) -> Bool {
|
||||
let schemes = includeDataURIs ? ["http", "https", "data"] : ["http", "https"]
|
||||
if let scheme = scheme, schemes.contains(scheme) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// This helps find local urls that we do not want to show loading bars on.
|
||||
// These utility pages should be invisible to the user
|
||||
public var isLocalUtility: Bool {
|
||||
guard self.isLocal else {
|
||||
return false
|
||||
}
|
||||
let utilityURLs = ["/errors", "/about/sessionrestore", "/about/home", "/reader-mode"]
|
||||
return utilityURLs.contains { self.path.startsWith($0) }
|
||||
}
|
||||
|
||||
public var isLocal: Bool {
|
||||
guard isWebPage(includeDataURIs: false) else {
|
||||
return false
|
||||
}
|
||||
// iOS forwards hostless URLs (e.g., http://:6571) to localhost.
|
||||
guard let host = host, !host.isEmpty else {
|
||||
return true
|
||||
}
|
||||
|
||||
return host.lowercased() == "localhost" || host == "127.0.0.1"
|
||||
}
|
||||
|
||||
public var isIPv6: Bool {
|
||||
return host?.contains(":") ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
Returns whether the URL's scheme is one of those listed on the official list of URI schemes.
|
||||
This only accepts permanent schemes: historical and provisional schemes are not accepted.
|
||||
*/
|
||||
public var schemeIsValid: Bool {
|
||||
guard let scheme = scheme else { return false }
|
||||
return permanentURISchemes.contains(scheme.lowercased())
|
||||
}
|
||||
|
||||
public func havingRemovedAuthorisationComponents() -> URL {
|
||||
guard var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: false) else {
|
||||
return self
|
||||
}
|
||||
urlComponents.user = nil
|
||||
urlComponents.password = nil
|
||||
if let url = urlComponents.url {
|
||||
return url
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions to deal with ReaderMode URLs
|
||||
|
||||
extension URL {
|
||||
public var isReaderModeURL: Bool {
|
||||
let scheme = self.scheme, host = self.host, path = self.path
|
||||
return scheme == "http" && host == "localhost" && path == "/reader-mode/page"
|
||||
}
|
||||
|
||||
public var decodeReaderModeURL: URL? {
|
||||
if self.isReaderModeURL {
|
||||
if let components = URLComponents(url: self, resolvingAgainstBaseURL: false), let queryItems = components.queryItems, queryItems.count == 1 {
|
||||
if let queryItem = queryItems.first, let value = queryItem.value {
|
||||
return URL(string: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func encodeReaderModeURL(_ baseReaderModeURL: String) -> URL? {
|
||||
if let encodedURL = absoluteString.addingPercentEncoding(withAllowedCharacters: CharacterSet.alphanumerics) {
|
||||
if let aboutReaderURL = URL(string: "\(baseReaderModeURL)?url=\(encodedURL)") {
|
||||
return aboutReaderURL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to deal with ErrorPage URLs
|
||||
|
||||
extension URL {
|
||||
public var isErrorPageURL: Bool {
|
||||
if let host = self.host {
|
||||
return self.scheme == "http" && host == "localhost" && path == "/errors/error.html"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public var originalURLFromErrorURL: URL? {
|
||||
let components = URLComponents(url: self, resolvingAgainstBaseURL: false)
|
||||
if let queryURL = components?.queryItems?.find({ $0.name == "url" })?.value {
|
||||
return URL(string: queryURL)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers to deal with About URLs
|
||||
extension URL {
|
||||
public var isAboutHomeURL: Bool {
|
||||
if let urlString = self.getQuery()["url"]?.unescape(), isErrorPageURL {
|
||||
let url = URL(string: urlString) ?? self
|
||||
return url.aboutComponent == "home"
|
||||
}
|
||||
return self.aboutComponent == "home"
|
||||
}
|
||||
|
||||
public var isAboutURL: Bool {
|
||||
return self.aboutComponent != nil
|
||||
}
|
||||
|
||||
/// If the URI is an about: URI, return the path after "about/" in the URI.
|
||||
/// For example, return "home" for "http://localhost:1234/about/home/#panel=0".
|
||||
public var aboutComponent: String? {
|
||||
let aboutPath = "/about/"
|
||||
guard let scheme = self.scheme, let host = self.host else {
|
||||
return nil
|
||||
}
|
||||
if scheme == "http" && host == "localhost" && path.startsWith(aboutPath) {
|
||||
return path.substring(from: aboutPath.endIndex)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//MARK: Private Helpers
|
||||
private extension URL {
|
||||
func publicSuffixFromHost( _ host: String, withAdditionalParts additionalPartCount: Int) -> String? {
|
||||
if host.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check edge case where the host is either a single or double '.'.
|
||||
if host.isEmpty || NSString(string: host).lastPathComponent == "." {
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* The following algorithm breaks apart the domain and checks each sub domain against the effective TLD
|
||||
* entries from the effective_tld_names.dat file. It works like this:
|
||||
*
|
||||
* Example Domain: test.bbc.co.uk
|
||||
* TLD Entry: bbc
|
||||
*
|
||||
* 1. Start off by checking the current domain (test.bbc.co.uk)
|
||||
* 2. Also store the domain after the next dot (bbc.co.uk)
|
||||
* 3. If we find an entry that matches the current domain (test.bbc.co.uk), perform the following checks:
|
||||
* i. If the domain is a wildcard AND the previous entry is not nil, then the current domain matches
|
||||
* since it satisfies the wildcard requirement.
|
||||
* ii. If the domain is normal (no wildcard) and we don't have anything after the next dot, then
|
||||
* currentDomain is a valid TLD
|
||||
* iii. If the entry we matched is an exception case, then the base domain is the part after the next dot
|
||||
*
|
||||
* On the next run through the loop, we set the new domain to check as the part after the next dot,
|
||||
* update the next dot reference to be the string after the new next dot, and check the TLD entries again.
|
||||
* If we reach the end of the host (nextDot = nil) and we haven't found anything, then we've hit the
|
||||
* top domain level so we use it by default.
|
||||
*/
|
||||
|
||||
let tokens = host.components(separatedBy: ".")
|
||||
let tokenCount = tokens.count
|
||||
var suffix: String?
|
||||
var previousDomain: String? = nil
|
||||
var currentDomain: String = host
|
||||
|
||||
for offset in 0..<tokenCount {
|
||||
// Store the offset for use outside of this scope so we can add additional parts if needed
|
||||
let nextDot: String? = offset + 1 < tokenCount ? tokens[offset + 1..<tokenCount].joined(separator: ".") : nil
|
||||
|
||||
if let entry = etldEntries?[currentDomain] {
|
||||
if entry.isWild && (previousDomain != nil) {
|
||||
suffix = previousDomain
|
||||
break
|
||||
} else if entry.isNormal || (nextDot == nil) {
|
||||
suffix = currentDomain
|
||||
break
|
||||
} else if entry.isException {
|
||||
suffix = nextDot
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
previousDomain = currentDomain
|
||||
if let nextDot = nextDot {
|
||||
currentDomain = nextDot
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var baseDomain: String?
|
||||
if additionalPartCount > 0 {
|
||||
if let suffix = suffix {
|
||||
// Take out the public suffixed and add in the additional parts we want.
|
||||
let literalFromEnd: NSString.CompareOptions = [NSString.CompareOptions.literal, // Match the string exactly.
|
||||
NSString.CompareOptions.backwards, // Search from the end.
|
||||
NSString.CompareOptions.anchored] // Stick to the end.
|
||||
let suffixlessHost = host.replacingOccurrences(of: suffix, with: "", options: literalFromEnd, range: nil)
|
||||
let suffixlessTokens = suffixlessHost.components(separatedBy: ".").filter { $0 != "" }
|
||||
let maxAdditionalCount = max(0, suffixlessTokens.count - additionalPartCount)
|
||||
let additionalParts = suffixlessTokens[maxAdditionalCount..<suffixlessTokens.count]
|
||||
let partsString = additionalParts.joined(separator: ".")
|
||||
baseDomain = [partsString, suffix].joined(separator: ".")
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
baseDomain = suffix
|
||||
}
|
||||
|
||||
return baseDomain
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URLProtectionSpace {
|
||||
|
||||
public func urlString() -> String {
|
||||
// If our host is empty, return nothing since it doesn't make sense to add the scheme or port.
|
||||
guard !host.isEmpty else {
|
||||
return ""
|
||||
}
|
||||
|
||||
var urlString: String
|
||||
if let p = `protocol` {
|
||||
urlString = "\(p)://\(host)"
|
||||
} else {
|
||||
urlString = host
|
||||
}
|
||||
|
||||
// Check for non-standard ports
|
||||
if port != 0 && port != 443 && port != 80 {
|
||||
urlString += ":\(port)"
|
||||
}
|
||||
|
||||
return urlString
|
||||
}
|
||||
}
|
||||
18
mobile/ios/Shared/Extensions/OptionalExtensions.swift
Normal file
18
mobile/ios/Shared/Extensions/OptionalExtensions.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/* A smarter ?? operator which allows the left hand/right hand arguments to not be
|
||||
* the same type. This is useful where we want to print the string representation
|
||||
* of an optional value that is not a string but want to return a string value when
|
||||
* a value is absent.
|
||||
*
|
||||
* For more informatin, check out Oleb's post:
|
||||
* https://oleb.net/blog/2016/12/optionals-string-interpolation/ */
|
||||
|
||||
infix operator ???: NilCoalescingPrecedence
|
||||
public func ???<T>(optional: T?, defaultValue: @autoclosure () -> String) -> String {
|
||||
return optional.map { String(describing: $0) } ?? defaultValue()
|
||||
}
|
||||
84
mobile/ios/Shared/Extensions/SetExtensions.swift
Normal file
84
mobile/ios/Shared/Extensions/SetExtensions.swift
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension SetIterator {
|
||||
mutating func take(_ n: Int) -> [Element]? {
|
||||
precondition(n >= 0)
|
||||
|
||||
if n == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
var count: Int = 0
|
||||
var out: [Element] = []
|
||||
|
||||
while count < n {
|
||||
count += 1
|
||||
guard let val = self.next() else {
|
||||
if out.isEmpty {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
out.append(val)
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
public extension Set {
|
||||
func withSubsetsOfSize(_ n: Int, f: (Set<Iterator.Element>) throws -> Void) rethrows {
|
||||
precondition(n > 0)
|
||||
|
||||
if self.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
if n > self.count {
|
||||
try f(self)
|
||||
return
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
try self.forEach { try f(Set([$0])) }
|
||||
return
|
||||
}
|
||||
|
||||
var generator = self.makeIterator()
|
||||
while let next = generator.take(n) {
|
||||
if !next.isEmpty {
|
||||
try f(Set(next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func subsetsOfSize(_ n: Int) -> [Set<Iterator.Element>] {
|
||||
precondition(n > 0)
|
||||
|
||||
if self.isEmpty {
|
||||
return []
|
||||
}
|
||||
|
||||
if n > self.count {
|
||||
return [self]
|
||||
}
|
||||
|
||||
if n == 1 {
|
||||
// Special case.
|
||||
return self.map({ Set([$0]) })
|
||||
}
|
||||
|
||||
var generator = self.makeIterator()
|
||||
var out: [Set<Iterator.Element>] = []
|
||||
out.reserveCapacity(self.count / n)
|
||||
while let next = generator.take(n) {
|
||||
if !next.isEmpty {
|
||||
out.append(Set(next))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
}
|
||||
107
mobile/ios/Shared/Extensions/StringExtensions.swift
Normal file
107
mobile/ios/Shared/Extensions/StringExtensions.swift
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
public func startsWith(_ other: String) -> Bool {
|
||||
// rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets.
|
||||
if other.isEmpty {
|
||||
return true
|
||||
}
|
||||
if let range = self.range(of: other,
|
||||
options: NSString.CompareOptions.anchored) {
|
||||
return range.lowerBound == self.startIndex
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func endsWith(_ other: String) -> Bool {
|
||||
// rangeOfString returns nil if other is empty, destroying the analogy with (ordered) sets.
|
||||
if other.isEmpty {
|
||||
return true
|
||||
}
|
||||
if let range = self.range(of: other,
|
||||
options: [NSString.CompareOptions.anchored, NSString.CompareOptions.backwards]) {
|
||||
return range.upperBound == self.endIndex
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func escape() -> String? {
|
||||
// We can't guaruntee that strings have a valid string encoding, as this is an entry point for tainted data,
|
||||
// we should be very careful about forcefully dereferencing optional types.
|
||||
// https://stackoverflow.com/questions/33558933/why-is-the-return-value-of-string-addingpercentencoding-optional#33558934
|
||||
let queryItemDividers = CharacterSet(charactersIn: "?=&")
|
||||
let allowedEscapes = CharacterSet.urlQueryAllowed.symmetricDifference(queryItemDividers)
|
||||
return self.addingPercentEncoding(withAllowedCharacters: allowedEscapes)
|
||||
}
|
||||
|
||||
func unescape() -> String? {
|
||||
return self.removingPercentEncoding
|
||||
}
|
||||
|
||||
/**
|
||||
Ellipsizes a String only if it's longer than `maxLength`
|
||||
|
||||
"ABCDEF".ellipsize(4)
|
||||
// "AB…EF"
|
||||
|
||||
:param: maxLength The maximum length of the String.
|
||||
|
||||
:returns: A String with `maxLength` characters or less
|
||||
*/
|
||||
func ellipsize(maxLength: Int) -> String {
|
||||
if (maxLength >= 2) && (self.characters.count > maxLength) {
|
||||
let index1 = self.characters.index(self.startIndex, offsetBy: (maxLength + 1) / 2) // `+ 1` has the same effect as an int ceil
|
||||
let index2 = self.characters.index(self.endIndex, offsetBy: maxLength / -2)
|
||||
|
||||
return self.substring(to: index1) + "…\u{2060}" + self.substring(from: index2)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
private var stringWithAdditionalEscaping: String {
|
||||
return self.replacingOccurrences(of: "|", with: "%7C", options: NSString.CompareOptions(), range: nil)
|
||||
}
|
||||
|
||||
public var asURL: URL? {
|
||||
// Firefox and NSURL disagree about the valid contents of a URL.
|
||||
// Let's escape | for them.
|
||||
// We'd love to use one of the more sophisticated CFURL* or NSString.* functions, but
|
||||
// none seem to be quite suitable.
|
||||
return URL(string: self) ??
|
||||
URL(string: self.stringWithAdditionalEscaping)
|
||||
}
|
||||
|
||||
/// Returns a new string made by removing the leading String characters contained
|
||||
/// in a given character set.
|
||||
public func stringByTrimmingLeadingCharactersInSet(_ set: CharacterSet) -> String {
|
||||
var trimmed = self
|
||||
while trimmed.rangeOfCharacter(from: set)?.lowerBound == trimmed.startIndex {
|
||||
trimmed.remove(at: trimmed.startIndex)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
/// Adds a newline at the closest space from the middle of a string.
|
||||
/// Example turning "Mark as Read" into "Mark as\n Read"
|
||||
public func stringSplitWithNewline() -> String {
|
||||
let mid = self.characters.count/2
|
||||
|
||||
let arr: [Int] = self.characters.indices.flatMap {
|
||||
if self.characters[$0] == " " {
|
||||
return self.distance(from: startIndex, to: $0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
guard let closest = arr.enumerated().min(by: { abs($0.1 - mid) < abs($1.1 - mid) }) else {
|
||||
return self
|
||||
}
|
||||
var newString = self
|
||||
newString.insert("\n", at: newString.characters.index(newString.characters.startIndex, offsetBy: closest.element))
|
||||
return newString
|
||||
}
|
||||
}
|
||||
31
mobile/ios/Shared/Extensions/UIColorExtensions.swift
Normal file
31
mobile/ios/Shared/Extensions/UIColorExtensions.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
private struct Color {
|
||||
var red: CGFloat
|
||||
var green: CGFloat
|
||||
var blue: CGFloat
|
||||
}
|
||||
|
||||
extension UIColor {
|
||||
/**
|
||||
* Initializes and returns a color object for the given RGB hex integer.
|
||||
*/
|
||||
public convenience init(rgb: Int) {
|
||||
self.init(
|
||||
red: CGFloat((rgb & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgb & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat((rgb & 0x0000FF) >> 0) / 255.0,
|
||||
alpha: 1)
|
||||
}
|
||||
|
||||
public convenience init(colorString: String) {
|
||||
var colorInt: UInt32 = 0
|
||||
Scanner(string: colorString).scanHexInt32(&colorInt)
|
||||
self.init(rgb: (Int) (colorInt))
|
||||
}
|
||||
}
|
||||
83
mobile/ios/Shared/Extensions/UIImageExtensions.swift
Normal file
83
mobile/ios/Shared/Extensions/UIImageExtensions.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SDWebImage
|
||||
|
||||
private let imageLock = NSLock()
|
||||
|
||||
extension UIImage {
|
||||
/// Despite docs that say otherwise, UIImage(data: NSData) isn't thread-safe (see bug 1223132).
|
||||
/// As a workaround, synchronize access to this initializer.
|
||||
/// This fix requires that you *always* use this over UIImage(data: NSData)!
|
||||
public static func imageFromDataThreadSafe(_ data: Data) -> UIImage? {
|
||||
imageLock.lock()
|
||||
let image = UIImage(data: data)
|
||||
imageLock.unlock()
|
||||
return image
|
||||
}
|
||||
|
||||
/// Generates a UIImage from GIF data by calling out to SDWebImage. The latter in turn uses UIImage(data: NSData)
|
||||
/// in certain cases so we have to synchronize calls (see bug 1223132).
|
||||
public static func imageFromGIFDataThreadSafe(_ data: Data) -> UIImage? {
|
||||
imageLock.lock()
|
||||
let image = UIImage.sd_animatedGIF(with: data)
|
||||
imageLock.unlock()
|
||||
return image
|
||||
}
|
||||
|
||||
public static func dataIsGIF(_ data: Data) -> Bool {
|
||||
guard data.count > 3 else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for "GIF" header to identify GIF images
|
||||
var header = [UInt8](repeating: 0, count: 3)
|
||||
data.copyBytes(to: &header, count: 3 * MemoryLayout<UInt8>.size)
|
||||
return header == [0x47, 0x49, 0x46]
|
||||
}
|
||||
|
||||
public static func createWithColor(_ size: CGSize, color: UIColor) -> UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
let rect = CGRect(origin: CGPoint.zero, size: size)
|
||||
color.setFill()
|
||||
context!.fill(rect)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image!
|
||||
}
|
||||
|
||||
public func createScaled(_ size: CGSize) -> UIImage {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 0)
|
||||
draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: size))
|
||||
let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return scaledImage!
|
||||
}
|
||||
|
||||
public static func templateImageNamed(_ name: String) -> UIImage? {
|
||||
return UIImage(named: name)?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
// TESTING ONLY: not for use in release/production code.
|
||||
// PNG comparison can return false negatives, be very careful using for non-equal comparison.
|
||||
// PNG comparison requires UIImages to be constructed the same way in order for the metadata block to match,
|
||||
// this function ensures that.
|
||||
//
|
||||
// This can be verified with this code:
|
||||
// let image = UIImage(named: "fxLogo")!
|
||||
// let data = UIImagePNGRepresentation(image)!
|
||||
// assert(data != UIImagePNGRepresentation(UIImage(data: data)!))
|
||||
@available(*, deprecated, message: "use only in testing code")
|
||||
public func isStrictlyEqual(to other: UIImage) -> Bool {
|
||||
// Must use same constructor for PNG metadata block to be the same.
|
||||
let imageA = UIImage(data: UIImagePNGRepresentation(self)!)!
|
||||
let imageB = UIImage(data: UIImagePNGRepresentation(other)!)!
|
||||
let dataA = UIImagePNGRepresentation(imageA)!
|
||||
let dataB = UIImagePNGRepresentation(imageB)!
|
||||
return dataA == dataB
|
||||
}
|
||||
}
|
||||
14
mobile/ios/Shared/Extensions/URLRequestExtensions.swift
Normal file
14
mobile/ios/Shared/Extensions/URLRequestExtensions.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension URLRequest {
|
||||
public enum Method: String {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case delete = "DELETE"
|
||||
case put = "PUT"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue