GLEGram 12.5 — Initial public release

Based on Swiftgram 12.5 (Telegram iOS 12.5).
All GLEGram features ported and organized in GLEGram/ folder.

Features: Ghost Mode, Saved Deleted Messages, Content Protection Bypass,
Font Replacement, Fake Profile, Chat Export, Plugin System, and more.

See CHANGELOG_12.5.md for full details.
This commit is contained in:
Leeksov
2026-04-06 09:48:12 +03:00
commit 4647310322
39685 changed files with 11052678 additions and 0 deletions
@@ -0,0 +1,79 @@
import Foundation
public extension Array {
func parallelMap<T>(transform: (Element) -> T) -> [T] {
var result = ContiguousArray<T?>(repeating: nil, count: count)
return result.withUnsafeMutableBufferPointer { buffer in
DispatchQueue.concurrentPerform(iterations: buffer.count) { idx in
buffer[idx] = transform(self[idx])
}
return buffer.map { $0! }
}
}
}
/// Holds a sorted array, created from specified sequence
/// This structure is needed for the cases, when some part of application requires array to be sorted, but don't trust any inputs :)
public struct SortedArray<T: Comparable> {
public let value: Array<T>
public init<S: Sequence>(_ value: S) where S.Element == T {
self.value = value.sorted()
}
}
public extension SortedArray {
/// Returns the first index in which an element of the collection satisfies the given predicate.
/// The collection assumed to be sorted. If collection is not have sorted values the result is undefined.
///
/// The idea is to get first index of a function for which the given predicate evaluates to true.
///
/// let values = [1,2,3,4,5]
/// let idx = values.firstIndexAssumingSorted(where: { $0 > 3 })
///
/// // false, false, false, true, true
/// // ^
/// // therefore idx == 3
///
/// - Parameter predicate: A closure that takes an element as its argument
/// and returns a Boolean value that indicates whether the passed element
/// represents a match.
///
/// - Returns: The index of the first element for which `predicate` returns
/// `true`. If no elements in the collection satisfy the given predicate,
/// returns `nil`.
///
/// - Complexity: O(log(*n*)), where *n* is the length of the collection.
@inlinable
func firstIndex(where predicate: (T) throws -> Bool) rethrows -> Int? {
// Predicate should divide a collection to two pairs of values
// "bad" values for which predicate returns `false``
// "good" values for which predicate return `true`
// false false false false false true true true
// ^
// The idea is to get _first_ index which for which the predicate returns `true`
let lastIndex = value.count
// The index that represents where bad values start
var badIndex = -1
// The index that represents where good values start
var goodIndex = lastIndex
var midIndex = (badIndex + goodIndex) / 2
while badIndex + 1 < goodIndex {
if try predicate(value[midIndex]) {
goodIndex = midIndex
} else {
badIndex = midIndex
}
midIndex = (badIndex + goodIndex) / 2
}
// We're out of bounds, no good items in array
if midIndex == lastIndex || goodIndex == lastIndex {
return nil
}
return goodIndex
}
}
+52
View File
@@ -0,0 +1,52 @@
//
// Atomic.swift
//
//
// Created by Vladislav Lisianskii on 23.02.2022.
//
import Foundation
@propertyWrapper
public final class Atomic<Value> {
private var value: Value
private let queue = DispatchQueue(
label: "com.xcodegencore.atomic.\(UUID().uuidString)",
qos: .utility,
attributes: .concurrent,
autoreleaseFrequency: .inherit,
target: .global()
)
public init(wrappedValue: Value) {
self.value = wrappedValue
}
public var wrappedValue: Value {
get {
queue.sync { value }
}
set {
queue.async(flags: .barrier) { [weak self] in
self?.value = newValue
}
}
}
/// Allows us to get the actual `Atomic` instance with the $
/// prefix.
public var projectedValue: Atomic<Value> {
return self
}
/// Modifies the protected value using `closure`.
public func with<R>(
_ closure: (inout Value) throws -> R
) rethrows -> R {
try queue.sync(flags: .barrier) {
try closure(&value)
}
}
}
+252
View File
@@ -0,0 +1,252 @@
//
// Created by Eric Firestone on 3/22/16.
// Copyright © 2016 Square, Inc. All rights reserved.
// Released under the Apache v2 License.
//
// Adapted from https://gist.github.com/blakemerryman/76312e1cbf8aec248167
// Adapted from https://gist.github.com/efirestone/ce01ae109e08772647eb061b3bb387c3
import Foundation
public let GlobBehaviorBashV3 = Glob.Behavior(
supportsGlobstar: false,
includesFilesFromRootOfGlobstar: false,
includesDirectoriesInResults: true,
includesFilesInResultsIfTrailingSlash: false
)
public let GlobBehaviorBashV4 = Glob.Behavior(
supportsGlobstar: true, // Matches Bash v4 with "shopt -s globstar" option
includesFilesFromRootOfGlobstar: true,
includesDirectoriesInResults: true,
includesFilesInResultsIfTrailingSlash: false
)
public let GlobBehaviorGradle = Glob.Behavior(
supportsGlobstar: true,
includesFilesFromRootOfGlobstar: true,
includesDirectoriesInResults: false,
includesFilesInResultsIfTrailingSlash: true
)
/**
Finds files on the file system using pattern matching.
*/
public class Glob: Collection {
/**
* Different glob implementations have different behaviors, so the behavior of this
* implementation is customizable.
*/
public struct Behavior {
// If true then a globstar ("**") causes matching to be done recursively in subdirectories.
// If false then "**" is treated the same as "*"
let supportsGlobstar: Bool
// If true the results from the directory where the globstar is declared will be included as well.
// For example, with the pattern "dir/**/*.ext" the fie "dir/file.ext" would be included if this
// property is true, and would be omitted if it's false.
let includesFilesFromRootOfGlobstar: Bool
// If false then the results will not include directory entries. This does not affect recursion depth.
let includesDirectoriesInResults: Bool
// If false and the last characters of the pattern are "**/" then only directories are returned in the results.
let includesFilesInResultsIfTrailingSlash: Bool
}
public static var defaultBehavior = GlobBehaviorBashV4
public static let defaultBlacklistedDirectories = ["node_modules", "Pods"]
@Atomic private var isDirectoryCache = [String: Bool]()
public let behavior: Behavior
public let blacklistedDirectories: [String]
var paths = [String]()
public var startIndex: Int { paths.startIndex }
public var endIndex: Int { paths.endIndex }
/// Initialize a glob
///
/// - Parameters:
/// - pattern: The pattern to use when building the list of matching directories.
/// - behavior: See individual descriptions on `Glob.Behavior` values.
/// - blacklistedDirectories: An array of directories to ignore at the root level of the project.
public init(pattern: String, behavior: Behavior = Glob.defaultBehavior, blacklistedDirectories: [String] = defaultBlacklistedDirectories) {
self.behavior = behavior
self.blacklistedDirectories = blacklistedDirectories
var adjustedPattern = pattern
let hasTrailingGlobstarSlash = pattern.hasSuffix("**/")
var includeFiles = !hasTrailingGlobstarSlash
if behavior.includesFilesInResultsIfTrailingSlash {
includeFiles = true
if hasTrailingGlobstarSlash {
// Grab the files too.
adjustedPattern += "*"
}
}
let patterns = behavior.supportsGlobstar ? expandGlobstar(pattern: adjustedPattern) : [adjustedPattern]
#if os(macOS)
paths = patterns.parallelMap { paths(usingPattern: $0, includeFiles: includeFiles) }.flatMap { $0 }
#else
// Parallel invocations of Glob on Linux seems to be causing unexpected crashes
paths = patterns.map { paths(usingPattern: $0, includeFiles: includeFiles) }.flatMap { $0 }
#endif
paths = Array(Set(paths)).sorted { lhs, rhs in
lhs.compare(rhs) != ComparisonResult.orderedDescending
}
clearCaches()
}
// MARK: Subscript Support
public subscript(i: Int) -> String {
paths[i]
}
// MARK: Protocol of IndexableBase
public func index(after i: Int) -> Int {
i + 1
}
// MARK: Private
private var globalFlags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK
private func executeGlob(pattern: UnsafePointer<CChar>, gt: UnsafeMutablePointer<glob_t>) -> Bool {
glob(pattern, globalFlags, nil, gt) == 0
}
private func expandGlobstar(pattern: String) -> [String] {
guard pattern.contains("**") else {
return [pattern]
}
var results = [String]()
var parts = pattern.components(separatedBy: "**")
let firstPart = parts.removeFirst()
var lastPart = parts.joined(separator: "**")
var directories: [URL]
if FileManager.default.fileExists(atPath: firstPart) {
do {
directories = try exploreDirectories(path: firstPart)
} catch {
directories = []
print("Error parsing file system item: \(error)")
}
} else {
directories = []
}
if behavior.includesFilesFromRootOfGlobstar {
// Check the base directory for the glob star as well.
directories.insert(URL(fileURLWithPath: firstPart), at: 0)
// Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/"
if lastPart.isEmpty {
results.append(firstPart)
}
}
if lastPart.isEmpty {
lastPart = "*"
}
for directory in directories {
let partiallyResolvedPattern = directory.appendingPathComponent(lastPart)
let standardizedPattern = (partiallyResolvedPattern.relativePath as NSString).standardizingPath
results.append(contentsOf: expandGlobstar(pattern: standardizedPattern))
}
return results
}
private func exploreDirectories(path: String) throws -> [URL] {
try FileManager.default.contentsOfDirectory(atPath: path)
.compactMap { subpath -> [URL]? in
if blacklistedDirectories.contains(subpath) {
return nil
}
let firstLevel = URL(fileURLWithPath: path).appendingPathComponent(subpath, isDirectory: true)
guard isDirectory(path: firstLevel.path) else {
return nil
}
var subDirs: [URL] = try FileManager.default.subpathsOfDirectory(atPath: firstLevel.path)
.compactMap { subpath -> URL? in
let full = firstLevel.appendingPathComponent(subpath, isDirectory: true)
return isDirectory(path: full.path) ? full : nil
}
subDirs.append(firstLevel)
return subDirs
}
.joined()
.array()
}
private func isDirectory(path: String) -> Bool {
if let isDirectory = isDirectoryCache[path] {
return isDirectory
}
var isDirectoryBool = ObjCBool(false)
let isDirectory = FileManager.default.fileExists(atPath: path, isDirectory: &isDirectoryBool) && isDirectoryBool.boolValue
$isDirectoryCache.with { isDirectoryCache in
isDirectoryCache[path] = isDirectory
}
return isDirectory
}
private func clearCaches() {
$isDirectoryCache.with { isDirectoryCache in
isDirectoryCache.removeAll()
}
}
private func paths(usingPattern pattern: String, includeFiles: Bool) -> [String] {
var gt = glob_t()
defer { globfree(&gt) }
if executeGlob(pattern: pattern, gt: &gt) {
return populateFiles(gt: gt, includeFiles: includeFiles)
}
return []
}
private func populateFiles(gt: glob_t, includeFiles: Bool) -> [String] {
var paths = [String]()
let includeDirectories = behavior.includesDirectoriesInResults
#if os(macOS)
let matches = Int(gt.gl_matchc)
#else
let matches = Int(gt.gl_pathc)
#endif
for i in 0..<matches {
if let path = String(validatingUTF8: gt.gl_pathv[i]!) {
if !includeFiles || !includeDirectories {
let isDirectory = self.isDirectory(path: path)
if (!includeFiles && !isDirectory) || (!includeDirectories && isDirectory) {
continue
}
}
paths.append(path)
}
}
return paths
}
}
private extension Sequence {
func array() -> [Element] {
Array(self)
}
}
+273
View File
@@ -0,0 +1,273 @@
// To date, adding CommonCrypto to a Swift framework is problematic. See:
// http://stackoverflow.com/questions/25248598/importing-commoncrypto-in-a-swift-framework
// We're using a subset and modified version of CryptoSwift as an alternative.
// The following is an altered source version that only includes MD5. The original software can be found at:
// https://github.com/krzyzanowskim/CryptoSwift
// This is the original copyright notice:
/*
Copyright (C) 2014 Marcin Krzyżanowski <marcin.krzyzanowski@gmail.com>
This software is provided 'as-is', without any express or implied warranty.
In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
- The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation is required.
- Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
- This notice may not be removed or altered from any source or binary distribution.
*/
import Foundation
extension String {
public var md5: String {
if let data = data(using: .utf8, allowLossyConversion: true) {
let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
Array(bytes)
}
let MD5Calculator = MD5(message)
let MD5Data = MD5Calculator.calculate()
var MD5String = String()
for c in MD5Data {
MD5String += String(format: "%02x", c)
}
return MD5String
} else {
return self
}
}
}
/** array of bytes, little-endian representation */
func arrayOfBytes<T>(_ value: T, length: Int? = nil) -> [UInt8] {
let totalBytes = length ?? (MemoryLayout<T>.size * 8)
let valuePointer = UnsafeMutablePointer<T>.allocate(capacity: 1)
valuePointer.pointee = value
let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in
var bytes = [UInt8](repeating: 0, count: totalBytes)
for j in 0..<min(MemoryLayout<T>.size, totalBytes) {
bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee
}
return bytes
}
#if swift(>=4.1)
valuePointer.deinitialize(count: 1)
valuePointer.deallocate()
#else
valuePointer.deinitialize()
valuePointer.deallocate(capacity: 1)
#endif
return bytes
}
extension Int {
/** Array of bytes with optional padding (little-endian) */
func bytes(_ totalBytes: Int = MemoryLayout<Int>.size) -> [UInt8] {
arrayOfBytes(self, length: totalBytes)
}
}
extension NSMutableData {
/** Convenient way to append bytes */
func appendBytes(_ arrayOfBytes: [UInt8]) {
append(arrayOfBytes, length: arrayOfBytes.count)
}
}
protocol HashProtocol {
var message: [UInt8] { get }
/** Common part for hash calculation. Prepare header data. */
func prepare(_ len: Int) -> [UInt8]
}
extension HashProtocol {
func prepare(_ len: Int) -> [UInt8] {
var tmpMessage = message
// Step 1. Append Padding Bits
tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message
// append "0" bit until message length in bits 448 (mod 512)
var msgLength = tmpMessage.count
var counter = 0
while msgLength % len != (len - 8) {
counter += 1
msgLength += 1
}
tmpMessage += [UInt8](repeating: 0, count: counter)
return tmpMessage
}
}
func toUInt32Array(_ slice: ArraySlice<UInt8>) -> [UInt32] {
var result = [UInt32]()
result.reserveCapacity(16)
for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout<UInt32>.size) {
let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24
let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16
let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8
let d3 = UInt32(slice[idx])
let val: UInt32 = d0 | d1 | d2 | d3
result.append(val)
}
return result
}
struct BytesIterator: IteratorProtocol {
let chunkSize: Int
let data: [UInt8]
init(chunkSize: Int, data: [UInt8]) {
self.chunkSize = chunkSize
self.data = data
}
var offset = 0
mutating func next() -> ArraySlice<UInt8>? {
let end = min(chunkSize, data.count - offset)
let result = data[offset..<offset + end]
offset += result.count
return result.count > 0 ? result : nil
}
}
struct BytesSequence: Sequence {
let chunkSize: Int
let data: [UInt8]
func makeIterator() -> BytesIterator {
BytesIterator(chunkSize: chunkSize, data: data)
}
}
func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 {
((value << bits) & 0xFFFF_FFFF) | (value >> (32 - bits))
}
class MD5: HashProtocol {
static let size = 16 // 128 / 8
let message: [UInt8]
init(_ message: [UInt8]) {
self.message = message
}
/** specifies the per-round shift amounts */
private let shifts: [UInt32] = [
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
]
/** binary integer part of the sines of integers (Radians) */
private let sines: [UInt32] = [
0xD76A_A478, 0xE8C7_B756, 0x2420_70DB, 0xC1BD_CEEE,
0xF57C_0FAF, 0x4787_C62A, 0xA830_4613, 0xFD46_9501,
0x6980_98D8, 0x8B44_F7AF, 0xFFFF_5BB1, 0x895C_D7BE,
0x6B90_1122, 0xFD98_7193, 0xA679_438E, 0x49B4_0821,
0xF61E_2562, 0xC040_B340, 0x265E_5A51, 0xE9B6_C7AA,
0xD62F_105D, 0x0244_1453, 0xD8A1_E681, 0xE7D3_FBC8,
0x21E1_CDE6, 0xC337_07D6, 0xF4D5_0D87, 0x455A_14ED,
0xA9E3_E905, 0xFCEF_A3F8, 0x676F_02D9, 0x8D2A_4C8A,
0xFFFA_3942, 0x8771_F681, 0x6D9D_6122, 0xFDE5_380C,
0xA4BE_EA44, 0x4BDE_CFA9, 0xF6BB_4B60, 0xBEBF_BC70,
0x289B_7EC6, 0xEAA1_27FA, 0xD4EF_3085, 0x4881D05,
0xD9D4_D039, 0xE6DB_99E5, 0x1FA2_7CF8, 0xC4AC_5665,
0xF429_2244, 0x432A_FF97, 0xAB94_23A7, 0xFC93_A039,
0x655B_59C3, 0x8F0C_CC92, 0xFFEF_F47D, 0x8584_5DD1,
0x6FA8_7E4F, 0xFE2C_E6E0, 0xA301_4314, 0x4E08_11A1,
0xF753_7E82, 0xBD3A_F235, 0x2AD7_D2BB, 0xEB86_D391,
]
private let hashes: [UInt32] = [0x6745_2301, 0xEFCD_AB89, 0x98BA_DCFE, 0x1032_5476]
func calculate() -> [UInt8] {
var tmpMessage = prepare(64)
tmpMessage.reserveCapacity(tmpMessage.count + 4)
// hash values
var hh = hashes
// Step 2. Append Length a 64-bit representation of lengthInBits
let lengthInBits = (message.count * 8)
let lengthBytes = lengthInBits.bytes(64 / 8)
tmpMessage += lengthBytes.reversed()
// Process the message in successive 512-bit chunks:
let chunkSizeBytes = 512 / 8 // 64
for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) {
// break chunk into sixteen 32-bit words M[j], 0 j 15
let M = toUInt32Array(chunk)
assert(M.count == 16, "Invalid array")
// Initialize hash value for this chunk:
var A: UInt32 = hh[0]
var B: UInt32 = hh[1]
var C: UInt32 = hh[2]
var D: UInt32 = hh[3]
var dTemp: UInt32 = 0
// Main loop
for j in 0..<sines.count {
var g = 0
var F: UInt32 = 0
switch j {
case 0...15:
F = (B & C) | ((~B) & D)
g = j
case 16...31:
F = (D & B) | (~D & C)
g = (5 * j + 1) % 16
case 32...47:
F = B ^ C ^ D
g = (3 * j + 5) % 16
case 48...63:
F = C ^ (B | (~D))
g = (7 * j) % 16
default:
break
}
dTemp = D
D = C
C = B
B = B &+ rotateLeft(A &+ F &+ sines[j] &+ M[g], bits: shifts[j])
A = dTemp
}
hh[0] = hh[0] &+ A
hh[1] = hh[1] &+ B
hh[2] = hh[2] &+ C
hh[3] = hh[3] &+ D
}
var result = [UInt8]()
result.reserveCapacity(hh.count / 4)
hh.forEach {
let itemLE = $0.littleEndian
let r1 = UInt8(itemLE & 0xFF)
let r2 = UInt8((itemLE >> 8) & 0xFF)
let r3 = UInt8((itemLE >> 16) & 0xFF)
let r4 = UInt8((itemLE >> 24) & 0xFF)
result += [r1, r2, r3, r4]
}
return result
}
}
// swiftlint:enable all
@@ -0,0 +1,84 @@
import Foundation
import PathKit
extension Path {
/// Returns a Path without any inner parent directory references.
///
/// Similar to `NSString.standardizingPath`, but works with relative paths.
///
/// ### Examples
/// - `a/b/../c` simplifies to `a/c`
/// - `../a/b` simplifies to `../a/b`
/// - `a/../../c` simplifies to `../c`
public func simplifyingParentDirectoryReferences() -> Path {
if !string.contains("..") { // Skip simplifying if its already simple
var string = self.string
while string.hasSuffix(Path.separator) { // Remove all trailing path separators
string.removeLast()
}
return Path(String(string))
}
return normalize().components.reduce(Path(), +)
}
/// Returns the relative path necessary to go from `base` to `self`.
///
/// Both paths must be absolute or relative paths.
/// - throws: Throws an error when the path types do not match, or when `base` has so many parent path components
/// that it refers to an unknown parent directory.
public func relativePath(from base: Path) throws -> Path {
enum PathArgumentError: Error {
/// Can't back out of an unknown parent directory
case unknownParentDirectory
/// It's impossible to determine the path between an absolute and a relative path
case unmatchedAbsolutePath
}
func pathComponents(for path: ArraySlice<String>, relativeTo base: ArraySlice<String>, memo: [String]) throws -> [String] {
switch (base.first, path.first) {
// Base case: Paths are equivalent
case (.none, .none):
return memo
// No path to backtrack from
case (.none, .some(let rhs)):
guard rhs != "." else {
// Skip . instead of appending it
return try pathComponents(for: path.dropFirst(), relativeTo: base, memo: memo)
}
return try pathComponents(for: path.dropFirst(), relativeTo: base, memo: memo + [rhs])
// Both sides have a common parent
case (.some(let lhs), .some(let rhs)) where memo.isEmpty && lhs == rhs:
return try pathComponents(for: path.dropFirst(), relativeTo: base.dropFirst(), memo: memo)
// `base` has a path to back out of
case (.some(let lhs), _):
guard lhs != ".." else {
throw PathArgumentError.unknownParentDirectory
}
guard lhs != "." else {
// Skip . instead of resolving it to ..
return try pathComponents(for: path, relativeTo: base.dropFirst(), memo: memo)
}
return try pathComponents(for: path, relativeTo: base.dropFirst(), memo: memo + [".."])
}
}
guard isAbsolute && base.isAbsolute || !isAbsolute && !base.isAbsolute else {
throw PathArgumentError.unmatchedAbsolutePath
}
return Path(components: try pathComponents(for: ArraySlice(simplifyingParentDirectoryReferences().components),
relativeTo: ArraySlice(base.simplifyingParentDirectoryReferences().components),
memo: []))
}
/// Returns whether `self` is a strict parent of `child`.
///
/// Both paths must be asbolute or relative paths.
public func isParent(of child: Path) throws -> Bool {
let relativePath = try child.relativePath(from: self)
return relativePath.components.allSatisfy { $0 != ".." }
}
}
@@ -0,0 +1,97 @@
import Foundation
// https://gist.github.com/kristopherjohnson/543687c763cd6e524c91
/// Find first differing character between two strings
///
/// :param: s1 First String
/// :param: s2 Second String
///
/// :returns: .DifferenceAtIndex(i) or .NoDifference
public func firstDifferenceBetweenStrings(_ s1: String, _ s2: String) -> FirstDifferenceResult {
let len1 = s1.count
let len2 = s2.count
let lenMin = min(len1, len2)
for i in 0..<lenMin {
if (s1 as NSString).character(at: i) != (s2 as NSString).character(at: i) {
return .DifferenceAtIndex(i)
}
}
if len1 < len2 {
return .DifferenceAtIndex(len1)
}
if len2 < len1 {
return .DifferenceAtIndex(len2)
}
return .NoDifference
}
/// Create a formatted String representation of difference between strings
///
/// :param: s1 First string
/// :param: s2 Second string
///
/// :returns: a string, possibly containing significant whitespace and newlines
public func prettyFirstDifferenceBetweenStrings(_ s1: String, _ s2: String, previewPrefixLength: Int = 25, previewSuffixLength: Int = 25) -> String {
let firstDifferenceResult = firstDifferenceBetweenStrings(s1, s2)
func diffString(at index: Int, _ s1: String, _ s2: String) -> String {
let markerArrow = "\u{2b06}" // ""
let ellipsis = "\u{2026}" // ""
/// Given a string and a range, return a string representing that substring.
///
/// If the range starts at a position other than 0, an ellipsis
/// will be included at the beginning.
///
/// If the range ends before the actual end of the string,
/// an ellipsis is added at the end.
func windowSubstring(_ s: String, _ range: NSRange) -> String {
let validRange = NSMakeRange(range.location, min(range.length, s.count - range.location))
let substring = (s as NSString).substring(with: validRange)
let prefix = range.location > 0 ? ellipsis : ""
let suffix = (s.count - range.location > range.length) ? ellipsis : ""
return "\(prefix)\(substring)\(suffix)"
}
// Show this many characters before and after the first difference
let windowLength = previewPrefixLength + 1 + previewSuffixLength
let windowIndex = max(index - previewPrefixLength, 0)
let windowRange = NSMakeRange(windowIndex, windowLength)
let sub1 = windowSubstring(s1, windowRange)
let sub2 = windowSubstring(s2, windowRange)
let markerPosition = min(previewSuffixLength, index) + (windowIndex > 0 ? 1 : 0)
let markerPrefix = String(repeating: " ", count: markerPosition)
let markerLine = "\(markerPrefix)\(markerArrow)"
return "Difference at index \(index):\n\(sub1)\n\(sub2)\n\(markerLine)"
}
switch firstDifferenceResult {
case .NoDifference: return "No difference"
case let .DifferenceAtIndex(index): return diffString(at: index, s1, s2)
}
}
/// Result type for firstDifferenceBetweenStrings()
public enum FirstDifferenceResult {
/// Strings are identical
case NoDifference
/// Strings differ at the specified index.
///
/// This could mean that characters at the specified index are different,
/// or that one string is longer than the other
case DifferenceAtIndex(Int)
}