Dactyloidae iOS initial commit

This commit is contained in:
wuggy 2026-06-26 21:04:09 -07:00
commit 7154a0497e
2123 changed files with 197052 additions and 0 deletions

View 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
}
}

View 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
}
}

View 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) }
}
}

View 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)
}
}

View 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())
}
}

View 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: [])
}
}

View 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)")
}
}
}
}
}

View 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*-_.")
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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)
}
}

View 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
}
}

View 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
}
}
}

View 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
}
}

View file

@ -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
}
}

View 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()
}

View 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
}
}

View 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)
// "ABEF"
: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
}
}

View 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))
}
}

View 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
}
}

View 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"
}
}