├── .github └── workflows │ └── swift.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── sqids │ └── Sqids.swift └── Tests └── sqidsTests ├── AlphabetTests.swift ├── Blocklist.swift ├── EncodeTests.swift └── MinLengthTests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Build 20 | run: swift build -v 21 | - name: Run tests 22 | run: swift test -v 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | **v0.1.2:** 4 | - Detection of integer overflows during decoding (@thanks to [gh123man](https://github.com/gh123man)) 5 | 6 | **v0.1.0:** 7 | - Initial implementation of [the spec](https://github.com/sqids/sqids-spec) 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sqids maintainers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | /* 3 | MIT License 4 | 5 | Copyright (c) 2023-present Sqids maintainers. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | */ 25 | 26 | import PackageDescription 27 | 28 | let package = Package( 29 | name: "sqids", 30 | products: [ 31 | // Products define the executables and libraries a package produces, making them visible to other packages. 32 | .library( 33 | name: "sqids", 34 | targets: ["sqids"] 35 | ), 36 | ], 37 | targets: [ 38 | // Targets are the basic building blocks of a package, defining a module or a test suite. 39 | // Targets can depend on other targets in this package and products from dependencies. 40 | .target( 41 | name: "sqids"), 42 | .testTarget( 43 | name: "sqidsTests", 44 | dependencies: ["sqids"]), 45 | ] 46 | ) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids Swift](https://sqids.org/swift) 2 | 3 | [![Github Actions](https://img.shields.io/github/actions/workflow/status/sqids/sqids-swift/swift.yml)](https://github.com/sqids/sqids-swift/actions) 4 | 5 | [Sqids](https://sqids.org/swift) (*pronounced "squids"*) is a small library that lets you **generate unique IDs from numbers**. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups. 6 | 7 | Features: 8 | 9 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 10 | - **Quick decoding** - easily decode IDs back into numbers 11 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 12 | - **ID padding** - provide minimum length to make IDs more uniform 13 | - **URL safe** - auto-generated IDs do not contain common profanity 14 | - **Randomized output** - Sequential input provides nonconsecutive IDs 15 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 16 | 17 | ## 🧰 Use-cases 18 | 19 | Good for: 20 | 21 | - Generating IDs for public URLs (eg: link shortening) 22 | - Generating IDs for internal systems (eg: event tracking) 23 | - Decoding for quicker database lookups (eg: by primary keys) 24 | 25 | Not good for: 26 | 27 | - Sensitive data (this is not an encryption library) 28 | - User IDs (can be decoded revealing user count) 29 | 30 | ## 🚀 Getting started 31 | 32 | Add the following dependency to your Swift `Package.swift`: 33 | 34 | ```swift 35 | dependencies.append( 36 | .package(url: "https://github.com/sqids/sqids-swift.git", from: "0.1.2") 37 | ) 38 | ``` 39 | 40 | Import the `Sqids` struct from the `sqids` framework: 41 | 42 | ```swift 43 | import sqids 44 | 45 | let sqids = Sqids() 46 | ``` 47 | 48 | ## 👩‍💻 Examples 49 | 50 | Simple encode & decode: 51 | 52 | ```swift 53 | let sqids = Sqids() 54 | let id = try sqids.encode([1, 2, 3]) // "86Rf07" 55 | let numbers = try sqids.decode(id) // [1, 2, 3] 56 | ``` 57 | 58 | > **Note** 59 | > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches. 60 | 61 | Enforce a *minimum* length for IDs: 62 | 63 | ```swift 64 | let sqids = Sqids(minLength: 10) 65 | let id = try sqids.encode([1, 2, 3]) // "86Rf07xd4z" 66 | let numbers = try sqids.decode(id) // [1, 2, 3] 67 | ``` 68 | 69 | Randomize IDs by providing a custom alphabet: 70 | 71 | ```swift 72 | let sqids = Sqids(alphabet: "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE") 73 | let id = try sqids.encode([1, 2, 3]) // "B4aajs" 74 | let numbers = try sqids.decode(id) // [1, 2, 3] 75 | ``` 76 | 77 | Prevent specific words from appearing anywhere in the auto-generated IDs: 78 | 79 | ```swift 80 | let sqids = Sqids(blocklist: ["86Rf07"]) 81 | let id = try sqids.encode([1, 2, 3]) // "se8ojk" 82 | let numbers = try sqids.decode(id) # [1, 2, 3] 83 | ``` 84 | 85 | ## 📝 License 86 | 87 | [MIT](LICENSE) 88 | -------------------------------------------------------------------------------- /Sources/sqids/Sqids.swift: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023-present Sqids maintainers. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import Foundation 26 | 27 | public struct Sqids { 28 | public typealias Id = Int64 29 | public typealias Ids = [Id] 30 | 31 | public enum Error: Swift.Error { 32 | case invalidMinLength(Int) 33 | case valueError(Id) 34 | case maximumAttemptsReached 35 | case overflow 36 | } 37 | public static let defaultAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 38 | public static let minAlphabetLength = 3 39 | public static let minLengthLimit = 255 40 | public static let minBlocklistWordLength = 3 41 | 42 | public let alphabet: [Character] 43 | public let minLength: Int 44 | public let blocklist: Set 45 | 46 | public init( 47 | alphabet: String = Self.defaultAlphabet, 48 | minLength: Int = 0, 49 | blocklist: [String]? = nil 50 | ) { 51 | let wordList = blocklist ?? _DEFAULT_BLOCKLIST 52 | 53 | assert( 54 | alphabet.count >= Self.minAlphabetLength, 55 | "Alphabet length must be at least \(Self.minAlphabetLength)" 56 | ) 57 | assert( 58 | minLength >= 0 && minLength <= Self.minLengthLimit, 59 | "Minimum length has to be between 0 and \(Self.minLengthLimit)" 60 | ) 61 | assert( 62 | alphabet.reduce(true, { $0 && $1.isASCII }), 63 | "Alphabet cannot contain multibyte characters." 64 | ) 65 | assert( 66 | Set(alphabet).count == alphabet.count, 67 | "Alphabet must contain unique characters." 68 | ) 69 | self.alphabet = Self.shuffle(alphabet: Array(alphabet)) 70 | self.minLength = minLength 71 | self.blocklist = Set(wordList.map({ $0.lowercased() }).filter {word in 72 | if word.count < Self.minAlphabetLength { 73 | return false 74 | } 75 | else { 76 | let alphabet = Set(alphabet.lowercased()) 77 | let characters = Array(word) 78 | let reduced = characters.filter { alphabet.contains($0) } 79 | return word.count == reduced.count 80 | } 81 | }) 82 | } 83 | 84 | public func encode(_ numbers: Ids) throws -> String { 85 | if numbers.isEmpty { 86 | return "" 87 | } 88 | else { 89 | for n in numbers { 90 | if n < 0 { 91 | throw Error.valueError(n) 92 | } 93 | } 94 | return try _encode(numbers) 95 | } 96 | } 97 | 98 | private func _encode(_ numbers: Ids) throws -> String { 99 | let count = Id(alphabet.count) 100 | 101 | for increment in 0.. result.count { 121 | result.append(alphabet[0]) 122 | while minLength > result.count { 123 | let range = 0.. [Character] { 138 | var alphabet = [Character](self.alphabet.suffix(from: offset)) 139 | 140 | alphabet.append(contentsOf: self.alphabet.prefix(offset)) 141 | return alphabet.reversed() 142 | } 143 | 144 | public func decode(_ id: String) throws -> [Id] { 145 | var result = [Id]() 146 | 147 | if !id.isEmpty { 148 | let characterSet = CharacterSet(alphabet.flatMap({ $0.unicodeScalars })) 149 | 150 | if id.unicodeScalars.reduce(true, { $0 && characterSet.contains($1) }) { 151 | let offset = alphabet.firstIndex(of: id.first!)! 152 | var alphabet = splitReverse(offset: offset) 153 | var value = String(Array(id).suffix(from: 1)) 154 | 155 | while !value.isEmpty { 156 | let separator = String(alphabet.first!) 157 | let chunks = value.components(separatedBy: separator) 158 | 159 | if !chunks.isEmpty { 160 | if chunks[0].isEmpty { 161 | break 162 | } 163 | else { 164 | let number = try toNumber( 165 | id: chunks[0], 166 | alphabet: Array(alphabet.suffix(from: 1)) 167 | ) 168 | result.append(number) 169 | if chunks.count > 1 { 170 | alphabet = Self.shuffle(alphabet: alphabet) 171 | } 172 | } 173 | value = chunks.count > 1 ? chunks.suffix(from: 1).joined(separator: separator) : "" 174 | } 175 | } 176 | } 177 | } 178 | return result 179 | } 180 | 181 | private func toId(number: Id, alphabet: [Character]) -> String { 182 | let count = Id(alphabet.count) 183 | var number = number 184 | var result = [Character]() 185 | 186 | while true { 187 | result.insert(alphabet[Int(number % count)], at: 0) 188 | number /= count 189 | if number <= 0 { 190 | break 191 | } 192 | } 193 | return String(result) 194 | } 195 | 196 | static func shuffle(alphabet: String) -> String { 197 | return String(shuffle(alphabet: Array(alphabet))) 198 | } 199 | 200 | static func shuffle(alphabet: [Character]) -> [Character] { 201 | var characters = Array(alphabet) 202 | var i = 0 203 | var j = characters.count - 1 204 | 205 | while j > 0 { 206 | let ci = Int(characters[i].asciiValue!) 207 | let cj = Int(characters[j].asciiValue!) 208 | let r = (i * j + ci + cj) % characters.count 209 | 210 | (characters[i], characters[r]) = (characters[r], characters[i]) 211 | i += 1 212 | j -= 1 213 | } 214 | return characters 215 | } 216 | 217 | func toNumber(id: String, alphabet: [Character]) throws -> Id { 218 | let count = Id(alphabet.count) 219 | 220 | return try id.reduce(0) { 221 | let (product, productOverflow) = $0.multipliedReportingOverflow(by: count) 222 | guard !productOverflow else { throw Error.overflow } 223 | 224 | let (sum, sumOverflow) = product.addingReportingOverflow(Id(alphabet.firstIndex(of: $1) ?? -1)) 225 | guard !sumOverflow else { throw Error.overflow } 226 | 227 | return sum 228 | } 229 | } 230 | 231 | public func isBlocked(id: String) -> Bool { 232 | let id = id.lowercased() 233 | 234 | for word in blocklist { 235 | if word.count <= id.count { 236 | if id.count <= 3 || word.count <= 3 { 237 | if id == word { 238 | return true 239 | } 240 | } 241 | else if word.contains(where: \.isNumber) { 242 | if id.hasPrefix(word) || id.hasSuffix(word) { 243 | return true 244 | } 245 | } 246 | else if id.range(of: word) != nil { 247 | return true 248 | } 249 | } 250 | } 251 | return false 252 | } 253 | } 254 | 255 | let _DEFAULT_BLOCKLIST = [ 256 | "0rgasm", 257 | "1d10t", 258 | "1d1ot", 259 | "1di0t", 260 | "1diot", 261 | "1eccacu10", 262 | "1eccacu1o", 263 | "1eccacul0", 264 | "1eccaculo", 265 | "1mbec11e", 266 | "1mbec1le", 267 | "1mbeci1e", 268 | "1mbecile", 269 | "a11upat0", 270 | "a11upato", 271 | "a1lupat0", 272 | "a1lupato", 273 | "aand", 274 | "ah01e", 275 | "ah0le", 276 | "aho1e", 277 | "ahole", 278 | "al1upat0", 279 | "al1upato", 280 | "allupat0", 281 | "allupato", 282 | "ana1", 283 | "ana1e", 284 | "anal", 285 | "anale", 286 | "anus", 287 | "arrapat0", 288 | "arrapato", 289 | "arsch", 290 | "arse", 291 | "ass", 292 | "b00b", 293 | "b00be", 294 | "b01ata", 295 | "b0ceta", 296 | "b0iata", 297 | "b0ob", 298 | "b0obe", 299 | "b0sta", 300 | "b1tch", 301 | "b1te", 302 | "b1tte", 303 | "ba1atkar", 304 | "balatkar", 305 | "bastard0", 306 | "bastardo", 307 | "batt0na", 308 | "battona", 309 | "bitch", 310 | "bite", 311 | "bitte", 312 | "bo0b", 313 | "bo0be", 314 | "bo1ata", 315 | "boceta", 316 | "boiata", 317 | "boob", 318 | "boobe", 319 | "bosta", 320 | "bran1age", 321 | "bran1er", 322 | "bran1ette", 323 | "bran1eur", 324 | "bran1euse", 325 | "branlage", 326 | "branler", 327 | "branlette", 328 | "branleur", 329 | "branleuse", 330 | "c0ck", 331 | "c0g110ne", 332 | "c0g11one", 333 | "c0g1i0ne", 334 | "c0g1ione", 335 | "c0gl10ne", 336 | "c0gl1one", 337 | "c0gli0ne", 338 | "c0glione", 339 | "c0na", 340 | "c0nnard", 341 | "c0nnasse", 342 | "c0nne", 343 | "c0u111es", 344 | "c0u11les", 345 | "c0u1l1es", 346 | "c0u1lles", 347 | "c0ui11es", 348 | "c0ui1les", 349 | "c0uil1es", 350 | "c0uilles", 351 | "c11t", 352 | "c11t0", 353 | "c11to", 354 | "c1it", 355 | "c1it0", 356 | "c1ito", 357 | "cabr0n", 358 | "cabra0", 359 | "cabrao", 360 | "cabron", 361 | "caca", 362 | "cacca", 363 | "cacete", 364 | "cagante", 365 | "cagar", 366 | "cagare", 367 | "cagna", 368 | "cara1h0", 369 | "cara1ho", 370 | "caracu10", 371 | "caracu1o", 372 | "caracul0", 373 | "caraculo", 374 | "caralh0", 375 | "caralho", 376 | "cazz0", 377 | "cazz1mma", 378 | "cazzata", 379 | "cazzimma", 380 | "cazzo", 381 | "ch00t1a", 382 | "ch00t1ya", 383 | "ch00tia", 384 | "ch00tiya", 385 | "ch0d", 386 | "ch0ot1a", 387 | "ch0ot1ya", 388 | "ch0otia", 389 | "ch0otiya", 390 | "ch1asse", 391 | "ch1avata", 392 | "ch1er", 393 | "ch1ng0", 394 | "ch1ngadaz0s", 395 | "ch1ngadazos", 396 | "ch1ngader1ta", 397 | "ch1ngaderita", 398 | "ch1ngar", 399 | "ch1ngo", 400 | "ch1ngues", 401 | "ch1nk", 402 | "chatte", 403 | "chiasse", 404 | "chiavata", 405 | "chier", 406 | "ching0", 407 | "chingadaz0s", 408 | "chingadazos", 409 | "chingader1ta", 410 | "chingaderita", 411 | "chingar", 412 | "chingo", 413 | "chingues", 414 | "chink", 415 | "cho0t1a", 416 | "cho0t1ya", 417 | "cho0tia", 418 | "cho0tiya", 419 | "chod", 420 | "choot1a", 421 | "choot1ya", 422 | "chootia", 423 | "chootiya", 424 | "cl1t", 425 | "cl1t0", 426 | "cl1to", 427 | "clit", 428 | "clit0", 429 | "clito", 430 | "cock", 431 | "cog110ne", 432 | "cog11one", 433 | "cog1i0ne", 434 | "cog1ione", 435 | "cogl10ne", 436 | "cogl1one", 437 | "cogli0ne", 438 | "coglione", 439 | "cona", 440 | "connard", 441 | "connasse", 442 | "conne", 443 | "cou111es", 444 | "cou11les", 445 | "cou1l1es", 446 | "cou1lles", 447 | "coui11es", 448 | "coui1les", 449 | "couil1es", 450 | "couilles", 451 | "cracker", 452 | "crap", 453 | "cu10", 454 | "cu1att0ne", 455 | "cu1attone", 456 | "cu1er0", 457 | "cu1ero", 458 | "cu1o", 459 | "cul0", 460 | "culatt0ne", 461 | "culattone", 462 | "culer0", 463 | "culero", 464 | "culo", 465 | "cum", 466 | "cunt", 467 | "d11d0", 468 | "d11do", 469 | "d1ck", 470 | "d1ld0", 471 | "d1ldo", 472 | "damn", 473 | "de1ch", 474 | "deich", 475 | "depp", 476 | "di1d0", 477 | "di1do", 478 | "dick", 479 | "dild0", 480 | "dildo", 481 | "dyke", 482 | "encu1e", 483 | "encule", 484 | "enema", 485 | "enf01re", 486 | "enf0ire", 487 | "enfo1re", 488 | "enfoire", 489 | "estup1d0", 490 | "estup1do", 491 | "estupid0", 492 | "estupido", 493 | "etr0n", 494 | "etron", 495 | "f0da", 496 | "f0der", 497 | "f0ttere", 498 | "f0tters1", 499 | "f0ttersi", 500 | "f0tze", 501 | "f0utre", 502 | "f1ca", 503 | "f1cker", 504 | "f1ga", 505 | "fag", 506 | "fica", 507 | "ficker", 508 | "figa", 509 | "foda", 510 | "foder", 511 | "fottere", 512 | "fotters1", 513 | "fottersi", 514 | "fotze", 515 | "foutre", 516 | "fr0c10", 517 | "fr0c1o", 518 | "fr0ci0", 519 | "fr0cio", 520 | "fr0sc10", 521 | "fr0sc1o", 522 | "fr0sci0", 523 | "fr0scio", 524 | "froc10", 525 | "froc1o", 526 | "froci0", 527 | "frocio", 528 | "frosc10", 529 | "frosc1o", 530 | "frosci0", 531 | "froscio", 532 | "fuck", 533 | "g00", 534 | "g0o", 535 | "g0u1ne", 536 | "g0uine", 537 | "gandu", 538 | "go0", 539 | "goo", 540 | "gou1ne", 541 | "gouine", 542 | "gr0gnasse", 543 | "grognasse", 544 | "haram1", 545 | "harami", 546 | "haramzade", 547 | "hund1n", 548 | "hundin", 549 | "id10t", 550 | "id1ot", 551 | "idi0t", 552 | "idiot", 553 | "imbec11e", 554 | "imbec1le", 555 | "imbeci1e", 556 | "imbecile", 557 | "j1zz", 558 | "jerk", 559 | "jizz", 560 | "k1ke", 561 | "kam1ne", 562 | "kamine", 563 | "kike", 564 | "leccacu10", 565 | "leccacu1o", 566 | "leccacul0", 567 | "leccaculo", 568 | "m1erda", 569 | "m1gn0tta", 570 | "m1gnotta", 571 | "m1nch1a", 572 | "m1nchia", 573 | "m1st", 574 | "mam0n", 575 | "mamahuev0", 576 | "mamahuevo", 577 | "mamon", 578 | "masturbat10n", 579 | "masturbat1on", 580 | "masturbate", 581 | "masturbati0n", 582 | "masturbation", 583 | "merd0s0", 584 | "merd0so", 585 | "merda", 586 | "merde", 587 | "merdos0", 588 | "merdoso", 589 | "mierda", 590 | "mign0tta", 591 | "mignotta", 592 | "minch1a", 593 | "minchia", 594 | "mist", 595 | "musch1", 596 | "muschi", 597 | "n1gger", 598 | "neger", 599 | "negr0", 600 | "negre", 601 | "negro", 602 | "nerch1a", 603 | "nerchia", 604 | "nigger", 605 | "orgasm", 606 | "p00p", 607 | "p011a", 608 | "p01la", 609 | "p0l1a", 610 | "p0lla", 611 | "p0mp1n0", 612 | "p0mp1no", 613 | "p0mpin0", 614 | "p0mpino", 615 | "p0op", 616 | "p0rca", 617 | "p0rn", 618 | "p0rra", 619 | "p0uff1asse", 620 | "p0uffiasse", 621 | "p1p1", 622 | "p1pi", 623 | "p1r1a", 624 | "p1rla", 625 | "p1sc10", 626 | "p1sc1o", 627 | "p1sci0", 628 | "p1scio", 629 | "p1sser", 630 | "pa11e", 631 | "pa1le", 632 | "pal1e", 633 | "palle", 634 | "pane1e1r0", 635 | "pane1e1ro", 636 | "pane1eir0", 637 | "pane1eiro", 638 | "panele1r0", 639 | "panele1ro", 640 | "paneleir0", 641 | "paneleiro", 642 | "patakha", 643 | "pec0r1na", 644 | "pec0rina", 645 | "pecor1na", 646 | "pecorina", 647 | "pen1s", 648 | "pendej0", 649 | "pendejo", 650 | "penis", 651 | "pip1", 652 | "pipi", 653 | "pir1a", 654 | "pirla", 655 | "pisc10", 656 | "pisc1o", 657 | "pisci0", 658 | "piscio", 659 | "pisser", 660 | "po0p", 661 | "po11a", 662 | "po1la", 663 | "pol1a", 664 | "polla", 665 | "pomp1n0", 666 | "pomp1no", 667 | "pompin0", 668 | "pompino", 669 | "poop", 670 | "porca", 671 | "porn", 672 | "porra", 673 | "pouff1asse", 674 | "pouffiasse", 675 | "pr1ck", 676 | "prick", 677 | "pussy", 678 | "put1za", 679 | "puta", 680 | "puta1n", 681 | "putain", 682 | "pute", 683 | "putiza", 684 | "puttana", 685 | "queca", 686 | "r0mp1ba11e", 687 | "r0mp1ba1le", 688 | "r0mp1bal1e", 689 | "r0mp1balle", 690 | "r0mpiba11e", 691 | "r0mpiba1le", 692 | "r0mpibal1e", 693 | "r0mpiballe", 694 | "rand1", 695 | "randi", 696 | "rape", 697 | "recch10ne", 698 | "recch1one", 699 | "recchi0ne", 700 | "recchione", 701 | "retard", 702 | "romp1ba11e", 703 | "romp1ba1le", 704 | "romp1bal1e", 705 | "romp1balle", 706 | "rompiba11e", 707 | "rompiba1le", 708 | "rompibal1e", 709 | "rompiballe", 710 | "ruff1an0", 711 | "ruff1ano", 712 | "ruffian0", 713 | "ruffiano", 714 | "s1ut", 715 | "sa10pe", 716 | "sa1aud", 717 | "sa1ope", 718 | "sacanagem", 719 | "sal0pe", 720 | "salaud", 721 | "salope", 722 | "saugnapf", 723 | "sb0rr0ne", 724 | "sb0rra", 725 | "sb0rrone", 726 | "sbattere", 727 | "sbatters1", 728 | "sbattersi", 729 | "sborr0ne", 730 | "sborra", 731 | "sborrone", 732 | "sc0pare", 733 | "sc0pata", 734 | "sch1ampe", 735 | "sche1se", 736 | "sche1sse", 737 | "scheise", 738 | "scheisse", 739 | "schlampe", 740 | "schwachs1nn1g", 741 | "schwachs1nnig", 742 | "schwachsinn1g", 743 | "schwachsinnig", 744 | "schwanz", 745 | "scopare", 746 | "scopata", 747 | "sexy", 748 | "sh1t", 749 | "shit", 750 | "slut", 751 | "sp0mp1nare", 752 | "sp0mpinare", 753 | "spomp1nare", 754 | "spompinare", 755 | "str0nz0", 756 | "str0nza", 757 | "str0nzo", 758 | "stronz0", 759 | "stronza", 760 | "stronzo", 761 | "stup1d", 762 | "stupid", 763 | "succh1am1", 764 | "succh1ami", 765 | "succhiam1", 766 | "succhiami", 767 | "sucker", 768 | "t0pa", 769 | "tapette", 770 | "test1c1e", 771 | "test1cle", 772 | "testic1e", 773 | "testicle", 774 | "tette", 775 | "topa", 776 | "tr01a", 777 | "tr0ia", 778 | "tr0mbare", 779 | "tr1ng1er", 780 | "tr1ngler", 781 | "tring1er", 782 | "tringler", 783 | "tro1a", 784 | "troia", 785 | "trombare", 786 | "turd", 787 | "twat", 788 | "vaffancu10", 789 | "vaffancu1o", 790 | "vaffancul0", 791 | "vaffanculo", 792 | "vag1na", 793 | "vagina", 794 | "verdammt", 795 | "verga", 796 | "w1chsen", 797 | "wank", 798 | "wichsen", 799 | "x0ch0ta", 800 | "x0chota", 801 | "xana", 802 | "xoch0ta", 803 | "xochota", 804 | "z0cc01a", 805 | "z0cc0la", 806 | "z0cco1a", 807 | "z0ccola", 808 | "z1z1", 809 | "z1zi", 810 | "ziz1", 811 | "zizi", 812 | "zocc01a", 813 | "zocc0la", 814 | "zocco1a", 815 | "zoccola", 816 | ] 817 | -------------------------------------------------------------------------------- /Tests/sqidsTests/AlphabetTests.swift: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2023-present Sqids maintainers. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | import XCTest 26 | @testable import sqids 27 | 28 | final class AlphabetTests: XCTestCase { 29 | func testSimple() throws { 30 | let sqids = Sqids(alphabet: "0123456789abcdef") 31 | let numbers: Sqids.Ids = [1, 2, 3] 32 | let id = "489158" 33 | 34 | XCTAssertEqual(try sqids.encode(numbers), id) 35 | XCTAssertEqual(try sqids.decode(id), numbers) 36 | } 37 | 38 | func testShortAlphabet() throws { 39 | let sqids = Sqids(alphabet: "abc") 40 | let numbers: Sqids.Ids = [1, 2, 3] 41 | let id = try sqids.encode(numbers) 42 | 43 | XCTAssertEqual("aacacbaa", id) 44 | XCTAssertEqual(try sqids.decode(id), numbers) 45 | } 46 | 47 | func testLongAlphabet() throws { 48 | let sqids = Sqids( 49 | alphabet: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:'\"/?.>,<`~" 50 | ) 51 | let numbers: Sqids.Ids = [1, 2, 3] 52 | let id = try sqids.encode(numbers) 53 | 54 | XCTAssertEqual("+;w