├── .clippy.toml ├── .gitattributes ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── deny.toml ├── release.toml ├── rustfmt.toml ├── src ├── blocklist.json └── lib.rs └── tests ├── alphabet.rs ├── blocklist.rs ├── encoding.rs └── minlength.rs /.clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 10 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | **/blocklist.json binary 2 | Cargo.lock binary -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | on: [push, pull_request] 3 | jobs: 4 | check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out 8 | uses: actions/checkout@v3 9 | - name: Install Rust 10 | uses: actions-rs/toolchain@v1 11 | with: 12 | profile: minimal 13 | toolchain: stable 14 | override: true 15 | components: rustfmt, clippy 16 | - name: Set up cargo cache 17 | uses: actions/cache@v3 18 | continue-on-error: false 19 | with: 20 | path: | 21 | ~/.cargo/bin/ 22 | ~/.cargo/registry/index/ 23 | ~/.cargo/registry/cache/ 24 | ~/.cargo/git/db/ 25 | target/ 26 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 27 | restore-keys: ${{ runner.os }}-cargo- 28 | - name: Lint 29 | run: | 30 | rustfmt **/*.rs 31 | cargo clippy --all -- -D warnings 32 | - name: Install cargo check tools 33 | run: | 34 | cargo install --locked cargo-deny || true 35 | cargo install --locked cargo-outdated || true 36 | - name: Check 37 | run: | 38 | cargo deny check 39 | cargo outdated --exit-code 1 40 | rm -rf ~/.cargo/advisory-db 41 | - name: Test 42 | run: cargo test --all -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | **v0.4.3:** 4 | - More tests for `is_blocked_id` 5 | - Cargo update 6 | 7 | **v0.4.2:** 8 | - Fix u64 overflow panic [[PR #5](https://github.com/sqids/sqids-rust/pull/7)] 9 | - Cargo update 10 | - `cargo deny` update 11 | 12 | **v0.4.1:** 13 | - Derive `Clone` trait [[PR #6](https://github.com/sqids/sqids-rust/pull/6)] 14 | - Cargo update 15 | 16 | **v0.4.0:** 17 | - Introduced `Sqids::builder()` [[PR #5](https://github.com/sqids/sqids-rust/pull/5)] 18 | - Cargo update 19 | - Docs cleanup 20 | 21 | **v0.3.1:** 22 | - Improvement: impl error for Error [[PR #3](https://github.com/sqids/sqids-rust/pull/3)] 23 | - Using `thiserror` 24 | - Cargo update 25 | 26 | **v0.3.0:** **⚠️ BREAKING CHANGE** 27 | - **Breaking change**: IDs change. Algorithm has been fine-tuned for better performance [[Issue #11](https://github.com/sqids/sqids-spec/issues/11)] 28 | - `alphabet` cannot contain multibyte characters 29 | - `min_length` was changed from `usize` to `u8` 30 | - Max blocklist re-encoding attempts has been capped at the length of the alphabet - 1 31 | - Minimum alphabet length has changed from 5 to 3 32 | - `min_value()` and `max_value()` functions have been removed 33 | 34 | **v0.2.1:** 35 | - Bug fix: spec update (PR #7): blocklist filtering in uppercase-only alphabet [[PR #7](https://github.com/sqids/sqids-spec/pull/7)] 36 | - Updating Github Actions to use stable toolchain instead of nightly 37 | - Cargo update 38 | 39 | **v0.2.0:** 40 | - Bug fix: test for decoding an invalid ID with a repeating reserved character 41 | - Cargo update 42 | 43 | **v0.1.1:** 44 | - Initial implementation of [the spec](https://github.com/sqids/sqids-spec) -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sqids" 3 | description = "Generate short unique ids from numbers." 4 | repository = "https://github.com/sqids/sqids-rust" 5 | documentation = "https://docs.rs/sqids" 6 | homepage = "https://sqids.org/rust" 7 | version = "0.4.3" 8 | license = "MIT" 9 | edition = "2021" 10 | readme = "README.md" 11 | keywords = ["ids", "encode", "short", "sqids", "hashids"] 12 | 13 | [dependencies] 14 | derive_builder = "0.20.2" 15 | serde = "1.0.217" 16 | serde_json = "1.0.134" 17 | thiserror = "2.0.9" 18 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids Rust](https://sqids.org/rust) 2 | 3 | [![Latest version](https://img.shields.io/crates/v/sqids.svg)](https://crates.io/crates/sqids) 4 | [![Github Actions](https://img.shields.io/github/actions/workflow/status/sqids/sqids-rust/tests.yml)](https://github.com/sqids/sqids-rust/actions) 5 | [![Docs](https://docs.rs/sqids/badge.svg)](https://docs.rs/sqids/latest/sqids/) 6 | [![Downloads](https://img.shields.io/crates/d/sqids)](https://crates.io/crates/sqids) 7 | 8 | [Sqids](https://sqids.org/rust) (*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. 9 | 10 | Features: 11 | 12 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 13 | - **Quick decoding** - easily decode IDs back into numbers 14 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 15 | - **ID padding** - provide minimum length to make IDs more uniform 16 | - **URL safe** - auto-generated IDs do not contain common profanity 17 | - **Randomized output** - Sequential input provides nonconsecutive IDs 18 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 19 | 20 | ## 🧰 Use-cases 21 | 22 | Good for: 23 | 24 | - Generating IDs for public URLs (eg: link shortening) 25 | - Generating IDs for internal systems (eg: event tracking) 26 | - Decoding for quicker database lookups (eg: by primary keys) 27 | 28 | Not good for: 29 | 30 | - Sensitive data (this is not an encryption library) 31 | - User IDs (can be decoded revealing user count) 32 | 33 | ## 🚀 Getting started 34 | 35 | Add using cargo: 36 | 37 | ```bash 38 | cargo add sqids 39 | ``` 40 | 41 | ## 👩‍💻 Examples 42 | 43 | Simple encode & decode: 44 | 45 | ```rust 46 | # use sqids::Sqids; 47 | let sqids = Sqids::default(); 48 | let id = sqids.encode(&[1, 2, 3])?; // "86Rf07" 49 | let numbers = sqids.decode(&id); // [1, 2, 3] 50 | # Ok::<(), sqids::Error>(()) 51 | ``` 52 | 53 | > **Note** 54 | > 🚧 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. 55 | 56 | Enforce a *minimum* length for IDs: 57 | 58 | ```rust 59 | # use sqids::Sqids; 60 | let sqids = Sqids::builder() 61 | .min_length(10) 62 | .build()?; 63 | let id = sqids.encode(&[1, 2, 3])?; // "86Rf07xd4z" 64 | let numbers = sqids.decode(&id); // [1, 2, 3] 65 | # Ok::<(), sqids::Error>(()) 66 | ``` 67 | 68 | Randomize IDs by providing a custom alphabet: 69 | 70 | ```rust 71 | # use sqids::Sqids; 72 | let sqids = Sqids::builder() 73 | .alphabet("FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE".chars().collect()) 74 | .build()?; 75 | let id = sqids.encode(&[1, 2, 3])?; // "B4aajs" 76 | let numbers = sqids.decode(&id); // [1, 2, 3] 77 | # Ok::<(), sqids::Error>(()) 78 | ``` 79 | 80 | Prevent specific words from appearing anywhere in the auto-generated IDs: 81 | 82 | ```rust 83 | # use sqids::Sqids; 84 | let sqids = Sqids::builder() 85 | .blocklist(["86Rf07".to_string()].into()) 86 | .build()?; 87 | let id = sqids.encode(&[1, 2, 3])?; // "se8ojk" 88 | let numbers = sqids.decode(&id); // [1, 2, 3] 89 | # Ok::<(), sqids::Error>(()) 90 | ``` 91 | 92 | ## 📝 License 93 | 94 | [MIT](LICENSE) 95 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [graph] 2 | all-features = true 3 | 4 | [advisories] 5 | version = 2 6 | ignore = [] 7 | 8 | [licenses] 9 | version = 2 10 | allow = [ 11 | "MIT", 12 | "Apache-2.0", 13 | "Unicode-3.0" 14 | ] 15 | 16 | exceptions = [] 17 | 18 | [bans] 19 | multiple-versions = "allow" 20 | wildcards = "deny" 21 | deny = [] 22 | 23 | [sources] 24 | unknown-registry = "deny" 25 | unknown-git = "deny" 26 | allow-registry = ["https://github.com/rust-lang/crates.io-index"] 27 | allow-git = [] -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | consolidate-commits = false 2 | consolidate-pushes = true -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | comment_width = 100 3 | hard_tabs = true 4 | edition = "2021" 5 | reorder_imports = true 6 | imports_granularity = "Crate" 7 | use_small_heuristics = "Max" 8 | wrap_comments = true 9 | binop_separator = "Back" 10 | trailing_comma = "Vertical" 11 | trailing_semicolon = true 12 | use_field_init_shorthand = true 13 | format_macro_bodies = true 14 | format_code_in_doc_comments = true 15 | -------------------------------------------------------------------------------- /src/blocklist.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0rgasm", 3 | "1d10t", 4 | "1d1ot", 5 | "1di0t", 6 | "1diot", 7 | "1eccacu10", 8 | "1eccacu1o", 9 | "1eccacul0", 10 | "1eccaculo", 11 | "1mbec11e", 12 | "1mbec1le", 13 | "1mbeci1e", 14 | "1mbecile", 15 | "a11upat0", 16 | "a11upato", 17 | "a1lupat0", 18 | "a1lupato", 19 | "aand", 20 | "ah01e", 21 | "ah0le", 22 | "aho1e", 23 | "ahole", 24 | "al1upat0", 25 | "al1upato", 26 | "allupat0", 27 | "allupato", 28 | "ana1", 29 | "ana1e", 30 | "anal", 31 | "anale", 32 | "anus", 33 | "arrapat0", 34 | "arrapato", 35 | "arsch", 36 | "arse", 37 | "ass", 38 | "b00b", 39 | "b00be", 40 | "b01ata", 41 | "b0ceta", 42 | "b0iata", 43 | "b0ob", 44 | "b0obe", 45 | "b0sta", 46 | "b1tch", 47 | "b1te", 48 | "b1tte", 49 | "ba1atkar", 50 | "balatkar", 51 | "bastard0", 52 | "bastardo", 53 | "batt0na", 54 | "battona", 55 | "bitch", 56 | "bite", 57 | "bitte", 58 | "bo0b", 59 | "bo0be", 60 | "bo1ata", 61 | "boceta", 62 | "boiata", 63 | "boob", 64 | "boobe", 65 | "bosta", 66 | "bran1age", 67 | "bran1er", 68 | "bran1ette", 69 | "bran1eur", 70 | "bran1euse", 71 | "branlage", 72 | "branler", 73 | "branlette", 74 | "branleur", 75 | "branleuse", 76 | "c0ck", 77 | "c0g110ne", 78 | "c0g11one", 79 | "c0g1i0ne", 80 | "c0g1ione", 81 | "c0gl10ne", 82 | "c0gl1one", 83 | "c0gli0ne", 84 | "c0glione", 85 | "c0na", 86 | "c0nnard", 87 | "c0nnasse", 88 | "c0nne", 89 | "c0u111es", 90 | "c0u11les", 91 | "c0u1l1es", 92 | "c0u1lles", 93 | "c0ui11es", 94 | "c0ui1les", 95 | "c0uil1es", 96 | "c0uilles", 97 | "c11t", 98 | "c11t0", 99 | "c11to", 100 | "c1it", 101 | "c1it0", 102 | "c1ito", 103 | "cabr0n", 104 | "cabra0", 105 | "cabrao", 106 | "cabron", 107 | "caca", 108 | "cacca", 109 | "cacete", 110 | "cagante", 111 | "cagar", 112 | "cagare", 113 | "cagna", 114 | "cara1h0", 115 | "cara1ho", 116 | "caracu10", 117 | "caracu1o", 118 | "caracul0", 119 | "caraculo", 120 | "caralh0", 121 | "caralho", 122 | "cazz0", 123 | "cazz1mma", 124 | "cazzata", 125 | "cazzimma", 126 | "cazzo", 127 | "ch00t1a", 128 | "ch00t1ya", 129 | "ch00tia", 130 | "ch00tiya", 131 | "ch0d", 132 | "ch0ot1a", 133 | "ch0ot1ya", 134 | "ch0otia", 135 | "ch0otiya", 136 | "ch1asse", 137 | "ch1avata", 138 | "ch1er", 139 | "ch1ng0", 140 | "ch1ngadaz0s", 141 | "ch1ngadazos", 142 | "ch1ngader1ta", 143 | "ch1ngaderita", 144 | "ch1ngar", 145 | "ch1ngo", 146 | "ch1ngues", 147 | "ch1nk", 148 | "chatte", 149 | "chiasse", 150 | "chiavata", 151 | "chier", 152 | "ching0", 153 | "chingadaz0s", 154 | "chingadazos", 155 | "chingader1ta", 156 | "chingaderita", 157 | "chingar", 158 | "chingo", 159 | "chingues", 160 | "chink", 161 | "cho0t1a", 162 | "cho0t1ya", 163 | "cho0tia", 164 | "cho0tiya", 165 | "chod", 166 | "choot1a", 167 | "choot1ya", 168 | "chootia", 169 | "chootiya", 170 | "cl1t", 171 | "cl1t0", 172 | "cl1to", 173 | "clit", 174 | "clit0", 175 | "clito", 176 | "cock", 177 | "cog110ne", 178 | "cog11one", 179 | "cog1i0ne", 180 | "cog1ione", 181 | "cogl10ne", 182 | "cogl1one", 183 | "cogli0ne", 184 | "coglione", 185 | "cona", 186 | "connard", 187 | "connasse", 188 | "conne", 189 | "cou111es", 190 | "cou11les", 191 | "cou1l1es", 192 | "cou1lles", 193 | "coui11es", 194 | "coui1les", 195 | "couil1es", 196 | "couilles", 197 | "cracker", 198 | "crap", 199 | "cu10", 200 | "cu1att0ne", 201 | "cu1attone", 202 | "cu1er0", 203 | "cu1ero", 204 | "cu1o", 205 | "cul0", 206 | "culatt0ne", 207 | "culattone", 208 | "culer0", 209 | "culero", 210 | "culo", 211 | "cum", 212 | "cunt", 213 | "d11d0", 214 | "d11do", 215 | "d1ck", 216 | "d1ld0", 217 | "d1ldo", 218 | "damn", 219 | "de1ch", 220 | "deich", 221 | "depp", 222 | "di1d0", 223 | "di1do", 224 | "dick", 225 | "dild0", 226 | "dildo", 227 | "dyke", 228 | "encu1e", 229 | "encule", 230 | "enema", 231 | "enf01re", 232 | "enf0ire", 233 | "enfo1re", 234 | "enfoire", 235 | "estup1d0", 236 | "estup1do", 237 | "estupid0", 238 | "estupido", 239 | "etr0n", 240 | "etron", 241 | "f0da", 242 | "f0der", 243 | "f0ttere", 244 | "f0tters1", 245 | "f0ttersi", 246 | "f0tze", 247 | "f0utre", 248 | "f1ca", 249 | "f1cker", 250 | "f1ga", 251 | "fag", 252 | "fica", 253 | "ficker", 254 | "figa", 255 | "foda", 256 | "foder", 257 | "fottere", 258 | "fotters1", 259 | "fottersi", 260 | "fotze", 261 | "foutre", 262 | "fr0c10", 263 | "fr0c1o", 264 | "fr0ci0", 265 | "fr0cio", 266 | "fr0sc10", 267 | "fr0sc1o", 268 | "fr0sci0", 269 | "fr0scio", 270 | "froc10", 271 | "froc1o", 272 | "froci0", 273 | "frocio", 274 | "frosc10", 275 | "frosc1o", 276 | "frosci0", 277 | "froscio", 278 | "fuck", 279 | "g00", 280 | "g0o", 281 | "g0u1ne", 282 | "g0uine", 283 | "gandu", 284 | "go0", 285 | "goo", 286 | "gou1ne", 287 | "gouine", 288 | "gr0gnasse", 289 | "grognasse", 290 | "haram1", 291 | "harami", 292 | "haramzade", 293 | "hund1n", 294 | "hundin", 295 | "id10t", 296 | "id1ot", 297 | "idi0t", 298 | "idiot", 299 | "imbec11e", 300 | "imbec1le", 301 | "imbeci1e", 302 | "imbecile", 303 | "j1zz", 304 | "jerk", 305 | "jizz", 306 | "k1ke", 307 | "kam1ne", 308 | "kamine", 309 | "kike", 310 | "leccacu10", 311 | "leccacu1o", 312 | "leccacul0", 313 | "leccaculo", 314 | "m1erda", 315 | "m1gn0tta", 316 | "m1gnotta", 317 | "m1nch1a", 318 | "m1nchia", 319 | "m1st", 320 | "mam0n", 321 | "mamahuev0", 322 | "mamahuevo", 323 | "mamon", 324 | "masturbat10n", 325 | "masturbat1on", 326 | "masturbate", 327 | "masturbati0n", 328 | "masturbation", 329 | "merd0s0", 330 | "merd0so", 331 | "merda", 332 | "merde", 333 | "merdos0", 334 | "merdoso", 335 | "mierda", 336 | "mign0tta", 337 | "mignotta", 338 | "minch1a", 339 | "minchia", 340 | "mist", 341 | "musch1", 342 | "muschi", 343 | "n1gger", 344 | "neger", 345 | "negr0", 346 | "negre", 347 | "negro", 348 | "nerch1a", 349 | "nerchia", 350 | "nigger", 351 | "orgasm", 352 | "p00p", 353 | "p011a", 354 | "p01la", 355 | "p0l1a", 356 | "p0lla", 357 | "p0mp1n0", 358 | "p0mp1no", 359 | "p0mpin0", 360 | "p0mpino", 361 | "p0op", 362 | "p0rca", 363 | "p0rn", 364 | "p0rra", 365 | "p0uff1asse", 366 | "p0uffiasse", 367 | "p1p1", 368 | "p1pi", 369 | "p1r1a", 370 | "p1rla", 371 | "p1sc10", 372 | "p1sc1o", 373 | "p1sci0", 374 | "p1scio", 375 | "p1sser", 376 | "pa11e", 377 | "pa1le", 378 | "pal1e", 379 | "palle", 380 | "pane1e1r0", 381 | "pane1e1ro", 382 | "pane1eir0", 383 | "pane1eiro", 384 | "panele1r0", 385 | "panele1ro", 386 | "paneleir0", 387 | "paneleiro", 388 | "patakha", 389 | "pec0r1na", 390 | "pec0rina", 391 | "pecor1na", 392 | "pecorina", 393 | "pen1s", 394 | "pendej0", 395 | "pendejo", 396 | "penis", 397 | "pip1", 398 | "pipi", 399 | "pir1a", 400 | "pirla", 401 | "pisc10", 402 | "pisc1o", 403 | "pisci0", 404 | "piscio", 405 | "pisser", 406 | "po0p", 407 | "po11a", 408 | "po1la", 409 | "pol1a", 410 | "polla", 411 | "pomp1n0", 412 | "pomp1no", 413 | "pompin0", 414 | "pompino", 415 | "poop", 416 | "porca", 417 | "porn", 418 | "porra", 419 | "pouff1asse", 420 | "pouffiasse", 421 | "pr1ck", 422 | "prick", 423 | "pussy", 424 | "put1za", 425 | "puta", 426 | "puta1n", 427 | "putain", 428 | "pute", 429 | "putiza", 430 | "puttana", 431 | "queca", 432 | "r0mp1ba11e", 433 | "r0mp1ba1le", 434 | "r0mp1bal1e", 435 | "r0mp1balle", 436 | "r0mpiba11e", 437 | "r0mpiba1le", 438 | "r0mpibal1e", 439 | "r0mpiballe", 440 | "rand1", 441 | "randi", 442 | "rape", 443 | "recch10ne", 444 | "recch1one", 445 | "recchi0ne", 446 | "recchione", 447 | "retard", 448 | "romp1ba11e", 449 | "romp1ba1le", 450 | "romp1bal1e", 451 | "romp1balle", 452 | "rompiba11e", 453 | "rompiba1le", 454 | "rompibal1e", 455 | "rompiballe", 456 | "ruff1an0", 457 | "ruff1ano", 458 | "ruffian0", 459 | "ruffiano", 460 | "s1ut", 461 | "sa10pe", 462 | "sa1aud", 463 | "sa1ope", 464 | "sacanagem", 465 | "sal0pe", 466 | "salaud", 467 | "salope", 468 | "saugnapf", 469 | "sb0rr0ne", 470 | "sb0rra", 471 | "sb0rrone", 472 | "sbattere", 473 | "sbatters1", 474 | "sbattersi", 475 | "sborr0ne", 476 | "sborra", 477 | "sborrone", 478 | "sc0pare", 479 | "sc0pata", 480 | "sch1ampe", 481 | "sche1se", 482 | "sche1sse", 483 | "scheise", 484 | "scheisse", 485 | "schlampe", 486 | "schwachs1nn1g", 487 | "schwachs1nnig", 488 | "schwachsinn1g", 489 | "schwachsinnig", 490 | "schwanz", 491 | "scopare", 492 | "scopata", 493 | "sexy", 494 | "sh1t", 495 | "shit", 496 | "slut", 497 | "sp0mp1nare", 498 | "sp0mpinare", 499 | "spomp1nare", 500 | "spompinare", 501 | "str0nz0", 502 | "str0nza", 503 | "str0nzo", 504 | "stronz0", 505 | "stronza", 506 | "stronzo", 507 | "stup1d", 508 | "stupid", 509 | "succh1am1", 510 | "succh1ami", 511 | "succhiam1", 512 | "succhiami", 513 | "sucker", 514 | "t0pa", 515 | "tapette", 516 | "test1c1e", 517 | "test1cle", 518 | "testic1e", 519 | "testicle", 520 | "tette", 521 | "topa", 522 | "tr01a", 523 | "tr0ia", 524 | "tr0mbare", 525 | "tr1ng1er", 526 | "tr1ngler", 527 | "tring1er", 528 | "tringler", 529 | "tro1a", 530 | "troia", 531 | "trombare", 532 | "turd", 533 | "twat", 534 | "vaffancu10", 535 | "vaffancu1o", 536 | "vaffancul0", 537 | "vaffanculo", 538 | "vag1na", 539 | "vagina", 540 | "verdammt", 541 | "verga", 542 | "w1chsen", 543 | "wank", 544 | "wichsen", 545 | "x0ch0ta", 546 | "x0chota", 547 | "xana", 548 | "xoch0ta", 549 | "xochota", 550 | "z0cc01a", 551 | "z0cc0la", 552 | "z0cco1a", 553 | "z0ccola", 554 | "z1z1", 555 | "z1zi", 556 | "ziz1", 557 | "zizi", 558 | "zocc01a", 559 | "zocc0la", 560 | "zocco1a", 561 | "zoccola" 562 | ] -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![allow(clippy::tabs_in_doc_comments)] 3 | #![doc = include_str!("../README.md")] 4 | 5 | // Make the link to the LICENSE in README.md work. 6 | #[cfg(doc)] 7 | #[doc = include_str!("../LICENSE")] 8 | /// 9 | /// --- 10 | /// **Note**: This is the crate's license and not an actual item. 11 | pub const LICENSE: () = (); 12 | 13 | use std::{cmp::min, collections::HashSet, result}; 14 | 15 | use derive_builder::Builder; 16 | use thiserror::Error; 17 | 18 | /// sqids Error type. 19 | #[derive(Error, Debug, Eq, PartialEq)] 20 | pub enum Error { 21 | /// Alphabet cannot contain multibyte characters 22 | /// 23 | /// ``` 24 | /// # use sqids::{Sqids, Error}; 25 | /// let error = Sqids::builder().alphabet("☃️🦀🔥".chars().collect()).build().unwrap_err(); 26 | /// assert_eq!(error, Error::AlphabetMultibyteCharacters); 27 | /// ``` 28 | #[error("Alphabet cannot contain multibyte characters")] 29 | AlphabetMultibyteCharacters, 30 | /// Alphabet length must be at least 3 31 | /// 32 | /// ``` 33 | /// # use sqids::{Sqids, Error}; 34 | /// let error = Sqids::builder().alphabet("ab".chars().collect()).build().unwrap_err(); 35 | /// assert_eq!(error, Error::AlphabetLength); 36 | /// ``` 37 | #[error("Alphabet length must be at least 3")] 38 | AlphabetLength, 39 | /// Alphabet must contain unique characters 40 | /// 41 | /// ``` 42 | /// # use sqids::{Sqids, Error}; 43 | /// let error = Sqids::builder().alphabet("aba".chars().collect()).build().unwrap_err(); 44 | /// assert_eq!(error, Error::AlphabetUniqueCharacters); 45 | /// ``` 46 | #[error("Alphabet must contain unique characters")] 47 | AlphabetUniqueCharacters, 48 | /// Reached max attempts to re-generate the ID 49 | /// 50 | /// ``` 51 | /// # use sqids::{Sqids, Error}; 52 | /// let sqids = Sqids::builder() 53 | /// .alphabet("abc".chars().collect()) 54 | /// .min_length(3) 55 | /// .blocklist(["aac".to_string(), "bba".to_string(), "ccb".to_string()].into()) 56 | /// .build() 57 | /// .unwrap(); 58 | /// let error = sqids.encode(&[1]).unwrap_err(); 59 | /// assert_eq!(error, Error::BlocklistMaxAttempts); 60 | /// ``` 61 | #[error("Reached max attempts to re-generate the ID")] 62 | BlocklistMaxAttempts, 63 | } 64 | 65 | /// type alias for Result 66 | pub type Result = result::Result; 67 | 68 | /// The default alphabet used when none is given when creating a [Sqids]. 69 | pub const DEFAULT_ALPHABET: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 70 | 71 | /// Returns the default blocklist when none is given when creating a [Sqids]. 72 | pub fn default_blocklist() -> HashSet { 73 | serde_json::from_str(include_str!("blocklist.json")).unwrap() 74 | } 75 | 76 | /// Options for creating a [Sqids]. 77 | #[derive(Debug)] 78 | pub struct Options { 79 | /// The [Sqids] alphabet. 80 | pub alphabet: String, 81 | /// The minimum length of a sqid. 82 | pub min_length: u8, 83 | /// Blocklist. When creating a sqid [Sqids] will try to avoid generating a string that begins 84 | /// with one of these. 85 | pub blocklist: HashSet, 86 | } 87 | 88 | impl Options { 89 | /// Create an [Options] object. 90 | pub fn new( 91 | alphabet: Option, 92 | min_length: Option, 93 | blocklist: Option>, 94 | ) -> Self { 95 | let mut options = Options::default(); 96 | 97 | if let Some(alphabet) = alphabet { 98 | options.alphabet = alphabet; 99 | } 100 | if let Some(min_length) = min_length { 101 | options.min_length = min_length; 102 | } 103 | if let Some(blocklist) = blocklist { 104 | options.blocklist = blocklist; 105 | } 106 | 107 | options 108 | } 109 | } 110 | 111 | impl Default for Options { 112 | fn default() -> Self { 113 | Options { 114 | alphabet: DEFAULT_ALPHABET.to_string(), 115 | min_length: 0, 116 | blocklist: default_blocklist(), 117 | } 118 | } 119 | } 120 | 121 | /// A generator for sqids. 122 | #[derive(Clone, Debug, Builder)] 123 | #[builder(build_fn(skip, error = "Error"), pattern = "owned")] 124 | pub struct Sqids { 125 | /// The alphabet that is being used when generating sqids. 126 | alphabet: Vec, 127 | /// The minimum length of a sqid. 128 | min_length: u8, 129 | /// Blocklist. When creating a sqid strings that begins 130 | /// with one of these will be avoided. 131 | blocklist: HashSet, 132 | } 133 | 134 | impl Default for Sqids { 135 | fn default() -> Self { 136 | Self::builder().build().unwrap() 137 | } 138 | } 139 | 140 | impl SqidsBuilder { 141 | /// Create a [SqidsBuilder]. 142 | pub fn new() -> Self { 143 | Self::default() 144 | } 145 | 146 | /// Build a [Sqids] object. 147 | pub fn build(self) -> Result { 148 | let alphabet: Vec = 149 | self.alphabet.unwrap_or_else(|| DEFAULT_ALPHABET.chars().collect()); 150 | 151 | for c in alphabet.iter() { 152 | if c.len_utf8() > 1 { 153 | return Err(Error::AlphabetMultibyteCharacters); 154 | } 155 | } 156 | 157 | if alphabet.len() < 3 { 158 | return Err(Error::AlphabetLength); 159 | } 160 | 161 | let unique_chars: HashSet = alphabet.iter().cloned().collect(); 162 | if unique_chars.len() != alphabet.len() { 163 | return Err(Error::AlphabetUniqueCharacters); 164 | } 165 | 166 | let lowercase_alphabet: Vec = 167 | alphabet.iter().map(|c| c.to_ascii_lowercase()).collect(); 168 | let filtered_blocklist: HashSet = self 169 | .blocklist 170 | .unwrap_or_else(default_blocklist) 171 | .iter() 172 | .filter_map(|word| { 173 | let word = word.to_lowercase(); 174 | if word.len() >= 3 && word.chars().all(|c| lowercase_alphabet.contains(&c)) { 175 | Some(word) 176 | } else { 177 | None 178 | } 179 | }) 180 | .collect(); 181 | 182 | Ok(Sqids { 183 | alphabet: Sqids::shuffle(&alphabet), 184 | min_length: self.min_length.unwrap_or(0), 185 | blocklist: filtered_blocklist, 186 | }) 187 | } 188 | } 189 | 190 | impl Sqids { 191 | /// Create a [Sqids] from [Options]. 192 | pub fn new(options: Option) -> Result { 193 | let options = options.unwrap_or_default(); 194 | Self::builder() 195 | .min_length(options.min_length) 196 | .alphabet(options.alphabet.chars().collect()) 197 | .blocklist(options.blocklist) 198 | .build() 199 | } 200 | 201 | /// Create a [SqidsBuilder]. 202 | pub fn builder() -> SqidsBuilder { 203 | SqidsBuilder::default() 204 | } 205 | 206 | /// Generate a sqid from a slice of numbers. 207 | /// 208 | /// When an sqid is generated it is checked against the [SqidsBuilder::blocklist]. When a 209 | /// blocked word is encountered another attempt is made by shifting the alphabet. 210 | /// When the alphabet is exhausted and all possible sqids for this input are blocked 211 | /// [Error::BlocklistMaxAttempts] is returned. 212 | pub fn encode(&self, numbers: &[u64]) -> Result { 213 | if numbers.is_empty() { 214 | return Ok(String::new()); 215 | } 216 | 217 | self.encode_numbers(numbers, 0) 218 | } 219 | 220 | /// Decode a sqid into a vector of numbers. When an invalid sqid is encountered an empty vector 221 | /// is returned. 222 | pub fn decode(&self, id: &str) -> Vec { 223 | let mut ret = Vec::new(); 224 | 225 | if id.is_empty() { 226 | return ret; 227 | } 228 | 229 | let alphabet_chars: HashSet = self.alphabet.iter().cloned().collect(); 230 | if !id.chars().all(|c| alphabet_chars.contains(&c)) { 231 | return ret; 232 | } 233 | 234 | let prefix = id.chars().next().unwrap(); 235 | let offset = self.alphabet.iter().position(|&c| c == prefix).unwrap(); 236 | let mut alphabet: Vec = 237 | self.alphabet.iter().cycle().skip(offset).take(self.alphabet.len()).copied().collect(); 238 | 239 | alphabet = alphabet.into_iter().rev().collect(); 240 | 241 | let mut id = id[1..].to_string(); 242 | 243 | while !id.is_empty() { 244 | let separator = alphabet[0]; 245 | 246 | let chunks: Vec<&str> = id.split(separator).collect(); 247 | if !chunks.is_empty() { 248 | if chunks[0].is_empty() { 249 | return ret; 250 | } 251 | 252 | let alphabet_without_separator: Vec = 253 | alphabet.iter().copied().skip(1).collect(); 254 | if let Some(value) = self.to_number(chunks[0], &alphabet_without_separator) { 255 | ret.push(value) 256 | } 257 | 258 | if chunks.len() > 1 { 259 | alphabet = Self::shuffle(&alphabet); 260 | } 261 | } 262 | 263 | id = chunks[1..].join(&separator.to_string()); 264 | } 265 | 266 | ret 267 | } 268 | 269 | fn encode_numbers(&self, numbers: &[u64], increment: usize) -> Result { 270 | if increment > self.alphabet.len() { 271 | return Err(Error::BlocklistMaxAttempts); 272 | } 273 | 274 | let mut offset = numbers.iter().enumerate().fold(numbers.len(), |a, (i, &v)| { 275 | self.alphabet[v as usize % self.alphabet.len()] as usize + i + a 276 | }) % self.alphabet.len(); 277 | 278 | offset = (offset + increment) % self.alphabet.len(); 279 | 280 | let mut alphabet: Vec = 281 | self.alphabet.iter().cycle().skip(offset).take(self.alphabet.len()).copied().collect(); 282 | 283 | let prefix = alphabet[0]; 284 | 285 | alphabet = alphabet.into_iter().rev().collect(); 286 | 287 | let mut ret: Vec = vec![prefix.to_string()]; 288 | 289 | for (i, &num) in numbers.iter().enumerate() { 290 | ret.push(self.to_id(num, &alphabet[1..])); 291 | 292 | if i < numbers.len() - 1 { 293 | ret.push(alphabet[0].to_string()); 294 | alphabet = Self::shuffle(&alphabet); 295 | } 296 | } 297 | 298 | let mut id = ret.join(""); 299 | 300 | if self.min_length as usize > id.len() { 301 | id += &alphabet[0].to_string(); 302 | 303 | while self.min_length as usize - id.len() > 0 { 304 | alphabet = Self::shuffle(&alphabet); 305 | 306 | let slice_len = min(self.min_length as usize - id.len(), alphabet.len()); 307 | let slice: Vec = alphabet.iter().take(slice_len).cloned().collect(); 308 | 309 | id += &slice.iter().collect::(); 310 | } 311 | } 312 | 313 | if self.is_blocked_id(&id) { 314 | id = self.encode_numbers(numbers, increment + 1)?; 315 | } 316 | 317 | Ok(id) 318 | } 319 | 320 | fn to_id(&self, num: u64, alphabet: &[char]) -> String { 321 | let mut id = Vec::new(); 322 | let mut result = num; 323 | 324 | loop { 325 | let idx = (result % alphabet.len() as u64) as usize; 326 | id.insert(0, alphabet[idx]); 327 | result /= alphabet.len() as u64; 328 | 329 | if result == 0 { 330 | break; 331 | } 332 | } 333 | 334 | id.into_iter().collect() 335 | } 336 | 337 | fn to_number(&self, id: &str, alphabet: &[char]) -> Option { 338 | let mut result: u64 = 0; 339 | let base = alphabet.len() as u64; 340 | 341 | for c in id.chars() { 342 | let idx = alphabet.iter().position(|&x| x == c).unwrap() as u64; 343 | 344 | if let Some(new_result) = result.checked_mul(base) { 345 | if let Some(final_result) = new_result.checked_add(idx) { 346 | result = final_result; 347 | } else { 348 | return None; 349 | } 350 | } else { 351 | return None; 352 | } 353 | } 354 | 355 | Some(result) 356 | } 357 | 358 | fn shuffle(alphabet: &[char]) -> Vec { 359 | let mut chars: Vec = alphabet.to_vec(); 360 | 361 | for i in 0..(chars.len() - 1) { 362 | let j = chars.len() - 1 - i; 363 | let r = (i as u32 * j as u32 + chars[i] as u32 + chars[j] as u32) % chars.len() as u32; 364 | chars.swap(i, r as usize); 365 | } 366 | 367 | chars 368 | } 369 | 370 | fn is_blocked_id(&self, id: &str) -> bool { 371 | let id = id.to_lowercase(); 372 | 373 | for word in &self.blocklist { 374 | if word.len() <= id.len() { 375 | if id.len() <= 3 || word.len() <= 3 { 376 | if id == *word { 377 | return true; 378 | } 379 | } else if word.chars().any(|c| c.is_ascii_digit()) { 380 | if id.starts_with(word) || id.ends_with(word) { 381 | return true; 382 | } 383 | } else if id.contains(word) { 384 | return true; 385 | } 386 | } 387 | } 388 | 389 | false 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /tests/alphabet.rs: -------------------------------------------------------------------------------- 1 | use sqids::*; 2 | 3 | #[test] 4 | fn simple() { 5 | let sqids = 6 | Sqids::new(Some(Options::new(Some("0123456789abcdef".to_string()), None, None))).unwrap(); 7 | 8 | let numbers = vec![1, 2, 3]; 9 | let id = "489158"; 10 | 11 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 12 | assert_eq!(sqids.decode(id), numbers); 13 | } 14 | 15 | #[test] 16 | fn short_alphabet() { 17 | let sqids = Sqids::new(Some(Options::new(Some("abc".to_string()), None, None))).unwrap(); 18 | 19 | let numbers = vec![1, 2, 3]; 20 | assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); 21 | } 22 | 23 | #[test] 24 | fn long_alphabet() { 25 | let sqids = Sqids::new(Some(Options::new( 26 | Some("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_+|{}[];:'\"/?.>,<`~".to_string()), 27 | None, 28 | None, 29 | ))).unwrap(); 30 | 31 | let numbers = vec![1, 2, 3]; 32 | assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); 33 | } 34 | 35 | #[test] 36 | fn multibyte_characters() { 37 | assert_eq!( 38 | Sqids::new(Some(Options::new(Some("ë1092".to_string()), None, None,))).err().unwrap(), 39 | Error::AlphabetMultibyteCharacters 40 | ) 41 | } 42 | 43 | #[test] 44 | fn repeating_alphabet_characters() { 45 | assert_eq!( 46 | Sqids::new(Some(Options::new(Some("aabcdefg".to_string()), None, None,))).err().unwrap(), 47 | Error::AlphabetUniqueCharacters 48 | ) 49 | } 50 | 51 | #[test] 52 | fn too_short_alphabet() { 53 | assert_eq!( 54 | Sqids::new(Some(Options::new(Some("ab".to_string()), None, None,))).err().unwrap(), 55 | Error::AlphabetLength 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /tests/blocklist.rs: -------------------------------------------------------------------------------- 1 | use sqids::*; 2 | use std::collections::HashSet; 3 | 4 | #[test] 5 | fn if_no_custom_blocklist_param_use_default_blocklist() { 6 | let sqids = Sqids::default(); 7 | 8 | assert_eq!(sqids.decode("aho1e"), vec![4572721]); 9 | assert_eq!(sqids.encode(&[4572721]).unwrap(), "JExTR"); 10 | } 11 | 12 | #[test] 13 | fn if_empty_blocklist_param_passed_dont_use_any_blocklist() { 14 | let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::new())))).unwrap(); 15 | 16 | assert_eq!(sqids.decode("aho1e"), vec![4572721]); 17 | assert_eq!(sqids.encode(&[4572721]).unwrap(), "aho1e"); 18 | } 19 | 20 | #[test] 21 | fn if_non_empty_blocklist_param_passed_use_only_that() { 22 | let sqids = 23 | Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["ArUO".to_string()]))))) 24 | .unwrap(); 25 | 26 | // make sure we don't use the default blocklist 27 | assert_eq!(sqids.decode("aho1e"), vec![4572721]); 28 | assert_eq!(sqids.encode(&[4572721]).unwrap(), "aho1e"); 29 | 30 | // make sure we are using the passed blocklist 31 | assert_eq!(sqids.decode("ArUO"), vec![100000]); 32 | assert_eq!(sqids.encode(&[100000]).unwrap(), "QyG4"); 33 | assert_eq!(sqids.decode("QyG4"), vec![100000]); 34 | } 35 | 36 | #[test] 37 | fn blocklist() { 38 | let sqids = Sqids::new(Some(Options::new( 39 | None, 40 | None, 41 | Some(HashSet::from([ 42 | "JSwXFaosAN".to_owned(), /* normal result of 1st encoding, let's block that word on 43 | * purpose */ 44 | "OCjV9JK64o".to_owned(), // result of 2nd encoding 45 | "rBHf".to_owned(), /* result of 3rd encoding is `4rBHfOiqd3`, let's block a 46 | * substring */ 47 | "79SM".to_owned(), // result of 4th encoding is `dyhgw479SM`, let's block the postfix 48 | "7tE6".to_owned(), // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix 49 | ])), 50 | ))) 51 | .unwrap(); 52 | 53 | assert_eq!(sqids.encode(&[1_000_000, 2_000_000]).unwrap(), "1aYeB7bRUt"); 54 | assert_eq!(sqids.decode("1aYeB7bRUt"), vec![1_000_000, 2_000_000]); 55 | } 56 | 57 | #[test] 58 | fn decoding_blocklist_words_should_still_work() { 59 | let sqids = Sqids::new(Some(Options::new( 60 | None, 61 | None, 62 | Some(HashSet::from([ 63 | "86Rf07".to_owned(), 64 | "se8ojk".to_owned(), 65 | "ARsz1p".to_owned(), 66 | "Q8AI49".to_owned(), 67 | "5sQRZO".to_owned(), 68 | ])), 69 | ))) 70 | .unwrap(); 71 | 72 | assert_eq!(sqids.decode("86Rf07"), vec![1, 2, 3]); 73 | assert_eq!(sqids.decode("se8ojk"), vec![1, 2, 3]); 74 | assert_eq!(sqids.decode("ARsz1p"), vec![1, 2, 3]); 75 | assert_eq!(sqids.decode("Q8AI49"), vec![1, 2, 3]); 76 | assert_eq!(sqids.decode("5sQRZO"), vec![1, 2, 3]); 77 | } 78 | 79 | #[test] 80 | fn match_against_short_blocklist_word() { 81 | let sqids = Sqids::new(Some(Options::new(None, None, Some(HashSet::from(["pnd".to_owned()]))))) 82 | .unwrap(); 83 | 84 | assert_eq!(sqids.decode(&sqids.encode(&[1000]).unwrap()), vec![1000]); 85 | } 86 | 87 | #[test] 88 | fn blocklist_filtering_in_constructor() { 89 | let sqids = Sqids::new(Some(Options::new( 90 | Some("ABCDEFGHIJKLMNOPQRSTUVWXYZ".to_string()), 91 | None, 92 | Some(HashSet::from(["sxnzkl".to_owned()])), /* lowercase blocklist in only-uppercase 93 | * alphabet */ 94 | ))) 95 | .unwrap(); 96 | 97 | let id = sqids.encode(&[1, 2, 3]).unwrap(); 98 | let numbers = sqids.decode(&id); 99 | 100 | assert_eq!(id, "IBSHOZ".to_string()); // without blocklist, would've been "SXNZKL" 101 | assert_eq!(numbers, vec![1, 2, 3]); 102 | } 103 | 104 | #[test] 105 | fn max_encoding_attempts() { 106 | let alphabet = "abc".to_string(); 107 | let min_length = 3; 108 | let blocklist = HashSet::from(["cab".to_owned(), "abc".to_owned(), "bca".to_owned()]); 109 | 110 | let sqids = Sqids::new(Some(Options::new( 111 | Some(alphabet.clone()), 112 | Some(min_length), 113 | Some(blocklist.clone()), 114 | ))) 115 | .unwrap(); 116 | 117 | assert_eq!(min_length as usize, alphabet.len()); 118 | assert_eq!(min_length as usize, blocklist.len()); 119 | 120 | assert_eq!(sqids.encode(&[0]).err().unwrap(), Error::BlocklistMaxAttempts); 121 | } 122 | 123 | #[test] 124 | fn specific_is_blocked_id_scenarios() { 125 | let sqids = Sqids::builder().blocklist(["hey".to_string()].into()).build().unwrap(); 126 | assert_eq!(sqids.encode(&[100]).unwrap(), "86u".to_string()); 127 | 128 | let sqids = Sqids::builder().blocklist(["86u".to_string()].into()).build().unwrap(); 129 | assert_eq!(sqids.encode(&[100]).unwrap(), "sec".to_string()); 130 | 131 | let sqids = Sqids::builder().blocklist(["vFo".to_string()].into()).build().unwrap(); 132 | assert_eq!(sqids.encode(&[1_000_000]).unwrap(), "gMvFo".to_string()); 133 | 134 | let sqids = Sqids::builder().blocklist(["lP3i".to_string()].into()).build().unwrap(); 135 | assert_eq!(sqids.encode(&[100, 202, 303, 404]).unwrap(), "oDqljxrokxRt".to_string()); 136 | 137 | let sqids = Sqids::builder().blocklist(["1HkYs".to_string()].into()).build().unwrap(); 138 | assert_eq!(sqids.encode(&[100, 202, 303, 404]).unwrap(), "oDqljxrokxRt".to_string()); 139 | 140 | let sqids = Sqids::builder().blocklist(["0hfxX".to_string()].into()).build().unwrap(); 141 | assert_eq!( 142 | sqids.encode(&[101, 202, 303, 404, 505, 606, 707]).unwrap(), 143 | "862REt0hfxXVdsLG8vGWD".to_string() 144 | ); 145 | 146 | let sqids = Sqids::builder().blocklist(["hfxX".to_string()].into()).build().unwrap(); 147 | assert_eq!( 148 | sqids.encode(&[101, 202, 303, 404, 505, 606, 707]).unwrap(), 149 | "seu8n1jO9C4KQQDxdOxsK".to_string() 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /tests/encoding.rs: -------------------------------------------------------------------------------- 1 | use sqids::*; 2 | 3 | #[test] 4 | fn simple() { 5 | let sqids = Sqids::default(); 6 | 7 | let numbers = vec![1, 2, 3]; 8 | let id = "86Rf07"; 9 | 10 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 11 | assert_eq!(sqids.decode(id), numbers); 12 | } 13 | 14 | #[test] 15 | fn different_inputs() { 16 | let sqids = Sqids::default(); 17 | 18 | let numbers = vec![0, 0, 0, 1, 2, 3, 100, 1_000, 100_000, 1_000_000, u64::MAX]; 19 | 20 | assert_eq!(sqids.decode(&sqids.encode(&numbers).unwrap()), numbers); 21 | } 22 | 23 | #[test] 24 | fn incremental_numbers() { 25 | let sqids = Sqids::default(); 26 | 27 | let ids = vec![ 28 | ("bM", vec![0]), 29 | ("Uk", vec![1]), 30 | ("gb", vec![2]), 31 | ("Ef", vec![3]), 32 | ("Vq", vec![4]), 33 | ("uw", vec![5]), 34 | ("OI", vec![6]), 35 | ("AX", vec![7]), 36 | ("p6", vec![8]), 37 | ("nJ", vec![9]), 38 | ]; 39 | 40 | for (id, numbers) in ids { 41 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 42 | assert_eq!(sqids.decode(id), numbers); 43 | } 44 | } 45 | 46 | #[test] 47 | fn incremental_numbers_same_index_0() { 48 | let sqids = Sqids::default(); 49 | 50 | let ids = vec![ 51 | ("SvIz", vec![0, 0]), 52 | ("n3qa", vec![0, 1]), 53 | ("tryF", vec![0, 2]), 54 | ("eg6q", vec![0, 3]), 55 | ("rSCF", vec![0, 4]), 56 | ("sR8x", vec![0, 5]), 57 | ("uY2M", vec![0, 6]), 58 | ("74dI", vec![0, 7]), 59 | ("30WX", vec![0, 8]), 60 | ("moxr", vec![0, 9]), 61 | ]; 62 | 63 | for (id, numbers) in ids { 64 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 65 | assert_eq!(sqids.decode(id), numbers); 66 | } 67 | } 68 | 69 | #[test] 70 | fn incremental_numbers_same_index_1() { 71 | let sqids = Sqids::default(); 72 | 73 | let ids = vec![ 74 | ("SvIz", vec![0, 0]), 75 | ("nWqP", vec![1, 0]), 76 | ("tSyw", vec![2, 0]), 77 | ("eX68", vec![3, 0]), 78 | ("rxCY", vec![4, 0]), 79 | ("sV8a", vec![5, 0]), 80 | ("uf2K", vec![6, 0]), 81 | ("7Cdk", vec![7, 0]), 82 | ("3aWP", vec![8, 0]), 83 | ("m2xn", vec![9, 0]), 84 | ]; 85 | 86 | for (id, numbers) in ids { 87 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 88 | assert_eq!(sqids.decode(id), numbers); 89 | } 90 | } 91 | 92 | #[test] 93 | fn multi_input() { 94 | let sqids = Sqids::default(); 95 | 96 | let numbers: Vec = (0..100).collect(); 97 | let output = sqids.decode(&sqids.encode(&numbers).unwrap()); 98 | 99 | assert_eq!(numbers, output); 100 | } 101 | 102 | #[test] 103 | fn encoding_no_numbers() { 104 | let sqids = Sqids::default(); 105 | assert_eq!(sqids.encode(&[]).unwrap(), ""); 106 | } 107 | 108 | #[test] 109 | fn decoding_empty_string() { 110 | let sqids = Sqids::default(); 111 | let numbers: Vec = vec![]; 112 | assert_eq!(sqids.decode(""), numbers); 113 | } 114 | 115 | #[test] 116 | fn decoding_invalid_character() { 117 | let sqids = Sqids::default(); 118 | let numbers: Vec = vec![]; 119 | assert_eq!(sqids.decode("*"), numbers); 120 | } 121 | 122 | #[test] 123 | fn decoding_number_maximum_value() { 124 | let sqids = Sqids::default(); 125 | let numbers = sqids.decode("ABARpJzdz9"); 126 | assert_eq!(numbers, [9_007_199_254_740_991]); // 2 ^ 53 127 | } 128 | 129 | #[test] 130 | fn decoding_number_overflows() { 131 | let sqids = Sqids::default(); 132 | let numbers = sqids.decode("0J4AEXRN106Z0"); // `https://github.com/sqids/sqids-rust/pull/7` 133 | assert_eq!(numbers, Vec::::new()); 134 | } 135 | -------------------------------------------------------------------------------- /tests/minlength.rs: -------------------------------------------------------------------------------- 1 | use sqids::*; 2 | 3 | #[test] 4 | fn simple() { 5 | let sqids = 6 | Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() as u8), None))) 7 | .unwrap(); 8 | 9 | let numbers = vec![1, 2, 3]; 10 | let id = "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM".to_owned(); 11 | 12 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 13 | assert_eq!(sqids.decode(&id), numbers); 14 | } 15 | 16 | #[test] 17 | fn incremental() { 18 | let numbers = [1, 2, 3]; 19 | let alphabet_length = Options::default().alphabet.len() as u8; 20 | 21 | let map = vec![ 22 | (6 as u8, "86Rf07".to_owned()), 23 | (7, "86Rf07x".to_owned()), 24 | (8, "86Rf07xd".to_owned()), 25 | (9, "86Rf07xd4".to_owned()), 26 | (10, "86Rf07xd4z".to_owned()), 27 | (11, "86Rf07xd4zB".to_owned()), 28 | (12, "86Rf07xd4zBm".to_owned()), 29 | (13, "86Rf07xd4zBmi".to_owned()), 30 | ( 31 | alphabet_length + 0, 32 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM".to_owned(), 33 | ), 34 | ( 35 | alphabet_length + 1, 36 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy".to_owned(), 37 | ), 38 | ( 39 | alphabet_length + 2, 40 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf".to_owned(), 41 | ), 42 | ( 43 | alphabet_length + 3, 44 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1".to_owned(), 45 | ), 46 | ]; 47 | 48 | for (min_length, id) in map { 49 | let sqids = Sqids::new(Some(Options::new(None, Some(min_length), None))).unwrap(); 50 | 51 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 52 | assert_eq!(sqids.encode(&numbers).unwrap().len(), min_length as usize); 53 | assert_eq!(sqids.decode(&id), numbers); 54 | } 55 | } 56 | 57 | #[test] 58 | fn incremental_numbers() { 59 | let sqids = 60 | Sqids::new(Some(Options::new(None, Some(Options::default().alphabet.len() as u8), None))) 61 | .unwrap(); 62 | 63 | let ids = vec![ 64 | ("SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu".to_owned(), vec![0, 0]), 65 | ("n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc".to_owned(), vec![0, 1]), 66 | ("tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ".to_owned(), vec![0, 2]), 67 | ("eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE".to_owned(), vec![0, 3]), 68 | ("rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX".to_owned(), vec![0, 4]), 69 | ("sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2".to_owned(), vec![0, 5]), 70 | ("uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0".to_owned(), vec![0, 6]), 71 | ("74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy".to_owned(), vec![0, 7]), 72 | ("30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS".to_owned(), vec![0, 8]), 73 | ("moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin".to_owned(), vec![0, 9]), 74 | ]; 75 | 76 | for (id, numbers) in ids { 77 | assert_eq!(sqids.encode(&numbers).unwrap(), id); 78 | assert_eq!(sqids.decode(&id), numbers); 79 | } 80 | } 81 | 82 | #[test] 83 | fn min_lengths() { 84 | for &min_length in &[0, 1, 5, 10, Options::default().alphabet.len() as u8] { 85 | for numbers in &[ 86 | vec![u64::MIN], 87 | vec![0, 0, 0, 0, 0], 88 | vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 89 | vec![100, 200, 300], 90 | vec![1_000, 2_000, 3_000], 91 | vec![1_000_000], 92 | vec![u64::MAX], 93 | ] { 94 | let sqids = Sqids::new(Some(Options::new(None, Some(min_length), None))).unwrap(); 95 | 96 | let id = sqids.encode(&numbers).unwrap(); 97 | assert!(id.len() >= min_length as usize); 98 | assert_eq!(sqids.decode(&id), *numbers); 99 | } 100 | } 101 | } 102 | --------------------------------------------------------------------------------