card-validate-2.4.0/.cargo_vcs_info.json0000644000000001360000000000100135370ustar { "git": { "sha1": "2895b600b1138550bd368fd30c6a38b12926937e" }, "path_in_vcs": "" }card-validate-2.4.0/.github/workflows/build.yml000064400000000000000000000011101046102023000175370ustar 00000000000000on: push: tags: - "v*.*.*" name: Build and Release jobs: release: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: stable override: true - name: Verify versions run: rustc --version && rustup --version && cargo --version - name: Release package run: cargo publish --no-verify --token ${CRATES_TOKEN} env: CRATES_TOKEN: ${{ secrets.CRATES_TOKEN }} card-validate-2.4.0/.github/workflows/test.yml000064400000000000000000000017731046102023000174360ustar 00000000000000on: [push, pull_request] name: Test and Build jobs: test: strategy: matrix: os: [ubuntu-latest] rust-toolchain: [stable, beta, nightly] fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v2 - name: Install Rust toolchain uses: actions-rs/toolchain@v1 with: toolchain: ${{ matrix.rust-toolchain }} components: rustfmt override: true - name: Verify versions run: rustc --version && rustup --version && cargo --version - name: Cache build artifacts id: cache-cargo uses: actions/cache@v2 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ matrix.rust-toolchain }} - name: Build code run: cargo build - name: Test code run: cargo test - name: Check code style run: cargo fmt -- --check card-validate-2.4.0/.gitignore000064400000000000000000000000221046102023000143110ustar 00000000000000target Cargo.lock card-validate-2.4.0/Cargo.toml0000644000000017670000000000100115500ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "card-validate" version = "2.4.0" authors = [ "Richard Protasov ", "Valerian Saliou ", ] description = "Rust card validate detects and validates credit card numbers" homepage = "https://github.com/valeriansaliou/rs-card-validate" readme = "README.md" license = "MIT" repository = "https://github.com/valeriansaliou/rs-card-validate.git" [lib] name = "card_validate" [dependencies.lazy_static] version = "1.0" [dependencies.regex] version = "1.0" card-validate-2.4.0/Cargo.toml.orig000064400000000000000000000007771046102023000152310ustar 00000000000000[package] name = "card-validate" version = "2.4.0" description = "Rust card validate detects and validates credit card numbers" readme = "README.md" license = "MIT" edition = "2021" homepage = "https://github.com/valeriansaliou/rs-card-validate" repository = "https://github.com/valeriansaliou/rs-card-validate.git" authors = [ "Richard Protasov ", "Valerian Saliou " ] [lib] name = "card_validate" [dependencies] lazy_static = "1.0" regex = "1.0" card-validate-2.4.0/LICENSE.md000064400000000000000000000022021046102023000137270ustar 00000000000000Copyright (c) 2015 Richard Protasov Copyright (c) 2017 Valerian Saliou Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. card-validate-2.4.0/README.md000064400000000000000000000031551046102023000136120ustar 00000000000000rs-card-validate ================ [![Test and Build](https://github.com/valeriansaliou/rs-card-validate/workflows/Test%20and%20Build/badge.svg?branch=master)](https://github.com/valeriansaliou/rs-card-validate/actions?query=workflow%3A%22Test+and+Build%22) [![Build and Release](https://github.com/valeriansaliou/rs-card-validate/workflows/Build%20and%20Release/badge.svg)](https://github.com/valeriansaliou/rs-card-validate/actions?query=workflow%3A%22Build+and+Release%22) [![Buy Me A Coffee](https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg)](https://www.buymeacoffee.com/valeriansaliou) [Documentation](https://docs.rs/crate/card-validate) [Crate](https://crates.io/crates/card-validate) Detects and validates credit card numbers (type of card, number length and Luhn checksum). **Important notice: this is a complete rework of [@rprotasov](https://github.com/rprotasov/creditcardvalidator) initial work, supporting more card providers and containing important validation fixes.** **🇫🇷 Crafted in Brest, France.** ## Supported providers **Debit cards:** * Visa Electron * Maestro * Forbrugsforeningen * Dankort **Credit cards:** * Visa * MasterCard * American Express * MIR * Diners Club * Discover * UnionPay * JCB ## Install library In your `Cargo.toml`: ```toml [dependencies] card-validate = "2.3" ``` ## Validate a number ```rust extern crate card_validate; use card_validate::Validate; let card_number = "5236313877109142"; match Validate::from(card_number) { Ok(result) => println!("Card type is: {}", result.card_type.name()), Err(err) => println!("Card is invalid: {:?}", err) } ``` card-validate-2.4.0/src/lib.rs000064400000000000000000000146621046102023000142430ustar 00000000000000//! card-validate detects and validates credit card numbers (type of card, number length and //! Luhn checksum). #[macro_use] extern crate lazy_static; extern crate regex; use regex::Regex; use std::ops::RangeInclusive; mod luhn; // The card formats have been copied from: https://github.com/faaez/creditcardutils/\ // blob/master/src/creditcardutils.coffee lazy_static! { static ref VISAELECTRON_PATTERN_REGEX: Regex = Regex::new(r"^4(026|17500|405|508|844|91[37])").unwrap(); static ref MAESTRO_PATTERN_REGEX: Regex = Regex::new(r"^(5(018|0[23]|[68])|6(39|7))").unwrap(); static ref FORBRUGSFORENINGEN_PATTERN_REGEX: Regex = Regex::new(r"^600").unwrap(); static ref DANKORT_PATTERN_REGEX: Regex = Regex::new(r"^5019").unwrap(); static ref VISA_PATTERN_REGEX: Regex = Regex::new(r"^4").unwrap(); static ref MIR_PATTERN_REGEX: Regex = Regex::new(r"^220[0-4]").unwrap(); static ref MASTERCARD_PATTERN_REGEX: Regex = Regex::new(r"^(5[1-5]|2[2-7])").unwrap(); static ref AMEX_PATTERN_REGEX: Regex = Regex::new(r"^3[47]").unwrap(); static ref DINERSCLUB_PATTERN_REGEX: Regex = Regex::new(r"^3[0689]").unwrap(); static ref DISCOVER_PATTERN_REGEX: Regex = Regex::new(r"^6([045]|22)").unwrap(); static ref UNIONPAY_PATTERN_REGEX: Regex = Regex::new(r"^(62|88)").unwrap(); static ref JCB_PATTERN_REGEX: Regex = Regex::new(r"^35").unwrap(); static ref OTHER_PATTERN_REGEX: Regex = Regex::new(r"^[0-9]+$").unwrap(); } /// Card type. Maps recognized cards, and validates their pattern and length. #[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] pub enum Type { // Debit VisaElectron, Maestro, Forbrugsforeningen, Dankort, // Credit Visa, MIR, MasterCard, Amex, DinersClub, Discover, UnionPay, JCB, } /// Validate error. Maps possible validation errors (eg. card number format invalid). #[derive(Clone, Copy, Debug, PartialEq)] #[non_exhaustive] pub enum ValidateError { InvalidFormat, InvalidLength, InvalidLuhn, UnknownType, } impl Type { pub fn name(&self) -> String { match self { Type::VisaElectron => "visaelectron", Type::Maestro => "maestro", Type::Forbrugsforeningen => "forbrugsforeningen", Type::Dankort => "dankort", Type::Visa => "visa", Type::MIR => "mir", Type::MasterCard => "mastercard", Type::Amex => "amex", Type::DinersClub => "dinersclub", Type::Discover => "discover", Type::UnionPay => "unionpay", Type::JCB => "jcb", } .to_string() } fn pattern(&self) -> &Regex { match self { Type::VisaElectron => &VISAELECTRON_PATTERN_REGEX, Type::Maestro => &MAESTRO_PATTERN_REGEX, Type::Forbrugsforeningen => &FORBRUGSFORENINGEN_PATTERN_REGEX, Type::Dankort => &DANKORT_PATTERN_REGEX, Type::Visa => &VISA_PATTERN_REGEX, Type::MIR => &MIR_PATTERN_REGEX, Type::MasterCard => &MASTERCARD_PATTERN_REGEX, Type::Amex => &AMEX_PATTERN_REGEX, Type::DinersClub => &DINERSCLUB_PATTERN_REGEX, Type::Discover => &DISCOVER_PATTERN_REGEX, Type::UnionPay => &UNIONPAY_PATTERN_REGEX, Type::JCB => &JCB_PATTERN_REGEX, } } fn length(&self) -> RangeInclusive { match self { Type::VisaElectron => 16..=16, Type::Maestro => 12..=19, Type::Forbrugsforeningen => 16..=16, Type::Dankort => 16..=16, Type::Visa => 13..=16, Type::MIR => 16..=19, Type::MasterCard => 16..=16, Type::Amex => 15..=15, Type::DinersClub => 14..=14, Type::Discover => 16..=16, Type::JCB => 16..=16, Type::UnionPay => 16..=19, } } const fn all() -> &'static [Type] { // Debit cards must come first, since they have more specific patterns than their // credit-card equivalents. &[ Type::VisaElectron, Type::Maestro, Type::Forbrugsforeningen, Type::Dankort, Type::Visa, Type::MIR, Type::MasterCard, Type::Amex, Type::DinersClub, Type::Discover, Type::UnionPay, Type::JCB, ] } } impl ToString for Type { fn to_string(&self) -> String { match self { Type::VisaElectron => "VisaElectron", Type::Maestro => "Maestro", Type::Forbrugsforeningen => "Forbrugsforeningen", Type::Dankort => "Dankort", Type::Visa => "Visa", Type::MIR => "MIR", Type::MasterCard => "MasterCard", Type::Amex => "Amex", Type::DinersClub => "DinersClub", Type::Discover => "Discover", Type::UnionPay => "UnionPay", Type::JCB => "JCB", } .to_string() } } /// Card validation utility. Used to validate a provided card number (length and Luhn checksum). #[derive(Clone, Copy, Debug, PartialEq)] pub struct Validate { pub card_type: Type, } impl Validate { pub fn from(card_number: &str) -> Result { let card_type = Validate::evaluate_type(card_number)?; if !Validate::is_length_valid(card_number, &card_type) { return Err(ValidateError::InvalidLength); } if !Validate::is_luhn_valid(card_number) { return Err(ValidateError::InvalidLuhn); } Ok(Validate { card_type }) } pub fn evaluate_type(card_number: &str) -> Result { // Validate overall card number structure if OTHER_PATTERN_REGEX.is_match(card_number) { for card in Type::all() { // Validate brand-specific card number structure if card.pattern().is_match(card_number) { return Ok(*card); } } Err(ValidateError::UnknownType) } else { Err(ValidateError::InvalidFormat) } } pub fn is_length_valid(card_number: &str, card_type: &Type) -> bool { let size = card_number.len(); let range = card_type.length(); range.contains(&size) } #[inline(always)] pub fn is_luhn_valid(card_number: &str) -> bool { luhn::valid(card_number) } } card-validate-2.4.0/src/luhn.rs000064400000000000000000000021101046102023000144240ustar 00000000000000//! card-validate detects and validates credit card numbers (type of card, number length and //! Luhn checksum). // Notice: this source code has been imported from: https://github.com/luhnmod10/rust // In an effort to reduce the amount of dependencies in the `card-validate` library. pub fn valid(number: &str) -> bool { let r_chars = number.chars().rev(); let range = 1..=number.chars().count(); let iter = range.zip(r_chars); let checksum = iter.fold(0, |mut checksum, (i, c)| { let is_odd = i % 2 == 1; if is_odd { checksum += checksum_modifier_odd(c); } else { checksum += checksum_modifier_even(c); }; checksum }); checksum % 10 == 0 } #[inline(always)] fn checksum_modifier_odd(c: char) -> u32 { numeric_char_to_u32(c) } #[inline(always)] fn checksum_modifier_even(c: char) -> u32 { let n = numeric_char_to_u32(c); let d = n * 2; if d <= 9 { d } else { d - 9 } } #[inline(always)] fn numeric_char_to_u32(c: char) -> u32 { (c as u32) - ('0' as u32) } card-validate-2.4.0/tests/validate.rs000064400000000000000000000144271046102023000156400ustar 00000000000000extern crate card_validate; use card_validate::{Validate, ValidateError}; fn visaelectron_numbers_valid() -> Vec<&'static str> { vec!["4917300800000000"] } fn maestro_numbers_valid() -> Vec<&'static str> { vec!["6759649826438453"] } fn forbrugsforeningen_numbers_valid() -> Vec<&'static str> { vec!["6007220000000004"] } fn dankort_numbers_valid() -> Vec<&'static str> { vec!["5019717010103742"] } fn visa_numbers_valid() -> Vec<&'static str> { vec![ "4539571147647251", "4532983409238819", "4485600412608021", "4916252910064718", "4916738103790259", ] } fn amex_numbers_valid() -> Vec<&'static str> { vec![ "343380440754432", "377156543570043", "340173808718013", "375801706141502", "372728319416034", ] } fn mastercard_numbers_valid() -> Vec<&'static str> { vec![ "5236313877109142", "5431604665471808", "5571788302926264", "5411516521560216", "5320524083396284", ] } fn discover_numbers_valid() -> Vec<&'static str> { vec![ "6011297718292606", "6011993225918523", "6011420510510997", "6011618637473995", "6011786207277235", ] } fn jcb_numbers_valid() -> Vec<&'static str> { vec!["3530111333300000", "3566002020360505"] } fn mir_numbers_valid() -> Vec<&'static str> { vec!["2200150220654583"] } fn unionpay_numbers_valid() -> Vec<&'static str> { vec![ "6271136264806203568", "6236265930072952775", "6204679475679144515", "6216657720782466507", ] } fn dinersclub_numbers_valid() -> Vec<&'static str> { vec![ "30569309025904", "38520000023237", "36700102000000", "36148900647913", ] } fn gibberish_numbers_invalid() -> Vec<&'static str> { vec!["zduhehiudIHZHIUZHUI", "0292DYYEFYFEFYEFEFIUH"] } fn unknown_numbers_invalid() -> Vec<&'static str> { vec!["00002837743671762", "1136283774"] } fn known_numbers_invalid() -> Vec<&'static str> { vec!["424242424", "4242424242424244242424242", "523631387710914"] } fn numbers_invalid_luhn() -> Vec<&'static str> { vec!["5236313877109141", "6011420510510995"] } fn valid_mixture() -> Vec<&'static str> { let card_types = vec![ visaelectron_numbers_valid(), maestro_numbers_valid(), forbrugsforeningen_numbers_valid(), dankort_numbers_valid(), visa_numbers_valid(), amex_numbers_valid(), mastercard_numbers_valid(), discover_numbers_valid(), jcb_numbers_valid(), unionpay_numbers_valid(), dinersclub_numbers_valid(), ]; let mut mixture = Vec::new(); for card_type in card_types { for number in card_type { mixture.push(number); } } mixture } #[test] fn valid_card() { for number in valid_mixture() { assert_eq!(Validate::from(number).is_ok(), true); } } #[test] fn gibberish_invalid() { for number in gibberish_numbers_invalid() { assert_eq!( Validate::from(number) == Err(ValidateError::InvalidFormat), true ); } } #[test] fn unknown_invalid() { for number in unknown_numbers_invalid() { assert_eq!( Validate::from(number) == Err(ValidateError::UnknownType), true ); } } #[test] fn known_invalid() { for number in known_numbers_invalid() { assert_eq!( Validate::from(number) == Err(ValidateError::InvalidLength), true ); } } #[test] fn invalid_luhn() { for number in numbers_invalid_luhn() { assert_eq!( Validate::from(number) == Err(ValidateError::InvalidLuhn), true ); } } #[test] fn correct_visaelectron_card_name() { for number in visaelectron_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "visaelectron".to_string()); } } #[test] fn correct_maestro_card_name() { for number in maestro_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "maestro".to_string()); } } #[test] fn correct_forbrugsforeningen_card_name() { for number in forbrugsforeningen_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "forbrugsforeningen".to_string()); } } #[test] fn correct_dankort_card_name() { for number in dankort_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "dankort".to_string()); } } #[test] fn correct_visa_card_name() { for number in visa_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "visa".to_string()); } } #[test] fn correct_amex_card_name() { for number in amex_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "amex".to_string()); } } #[test] fn correct_mastercard_card_name() { for number in mastercard_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "mastercard".to_string()); } } #[test] fn correct_discover_card_name() { for number in discover_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "discover".to_string()); } } #[test] fn correct_jcb_card_name() { for number in jcb_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "jcb".to_string()); } } #[test] fn correct_mir_card_name() { for number in mir_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "mir".to_string()); } } #[test] fn correct_unionpay_card_name() { for number in unionpay_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "unionpay".to_string()); } } #[test] fn correct_dinersclub_card_name() { for number in dinersclub_numbers_valid() { let result = Validate::from(number).unwrap(); assert_eq!(result.card_type.name(), "dinersclub".to_string()); } }