Last active
November 5, 2016 00:30
-
-
Save mrkishi/5010bcf8fc09252334d9dcfb230d7e6c to your computer and use it in GitHub Desktop.
AAMVA Magnetic Stripe Card Parser
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!doctype html> | |
<html lang="en"> | |
<head> | |
<meta charset="utf-8"> | |
<title>AAMVA Magnetic Stripe Card Parser</title> | |
<style> | |
html { | |
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
} | |
output { | |
display: block; | |
white-space: pre; | |
} | |
</style> | |
</head> | |
<body> | |
<textarea cols="84" rows="3" autofocus></textarea> | |
<output></output> | |
<script> | |
const input = document.querySelector('textarea') | |
const output = document.querySelector('output') | |
const parse = (() => { | |
const start_sentinel1 = `\\%` | |
const field_separator1 = `\\^` | |
const field1 = `[^${field_separator1}]` | |
const start_sentinel2 = `\\;` | |
const field_separator2 = `\\=` | |
const field2 = `[^${field_separator2}]` | |
const start_sentinel3 = `[\\#\\%\\+]` | |
const lrc = `.?` | |
const end_sentinel = `\\?` | |
const regex = new RegExp('' | |
// track 1 | |
+ start_sentinel1 | |
+ `(.{2})` // 1: state | |
+ `(${field1}{0,13})${field_separator1}?` // 2: city | |
+ `(${field1}{0,35})${field_separator1}?` // 3: name | |
+ `(${field1}*)${field_separator1}?` // 4: address | |
+ end_sentinel | |
+ lrc | |
// track 2 | |
+ start_sentinel2 | |
+ `(?:(?:` | |
+ `(${field2}{6})` // 5: IIN | |
+ `(${field2}{0,13})${field_separator2}` // 6: DL/ID | |
+ `(..)(..)` // 7, 8: expiration date (y, m) | |
+ `..(..)(..)(..)` // 9, 10, 11: birth date (y, m, d) | |
+ `(${field2}{0,5})${field_separator2}?` // 12: DL/ID overflow | |
+ `)|(?:.))` | |
+ end_sentinel | |
+ lrc | |
// track 3 | |
+ start_sentinel3 | |
+ `(.)` // 13: template version | |
+ `(.)` // 14: security version | |
+ `(.{11})` // 15: postal code | |
+ `(.{2})` // 16: class | |
+ `(.{10})` // 17: restrictions | |
+ `(.{4})` // 18: endorsements | |
+ `(.{1})` // 19: sex | |
+ `(.{3})` // 20: height | |
+ `(.{3})` // 21: weight | |
+ `(.{3})` // 22: hair color | |
+ `(.{3})` // 23: eyes color | |
/* | |
+ `(.{10})?` // 24: id | |
+ `(.{16})?` // 25: reserved | |
+ `(.{6})?` // 26: error correction | |
+ `(.{5})?` // 27: security | |
+ end_sentinel | |
+ lrc | |
*/ | |
) | |
const delimiter = `$` | |
const delimiters = /\$/g | |
const sex = { | |
'1': 'MALE', | |
'M': 'MALE', | |
'2': 'FEMALE', | |
'F': 'FEMALE', | |
} | |
return (text) => { | |
const data = text.value.replace(/[\r\n]/g, '') | |
const m = data.match(regex) | |
if (!m) { | |
return null | |
} | |
return { | |
state: m[1], | |
city: m[2], | |
name: m[3].replace(delimiter, ', ').replace(delimiter, ' ').trim(), | |
address: m[4].replace(delimiters, '\n').trim(), | |
iin: m[5], | |
dl_id: m[6] ? (() => { | |
if (m[1] === 'FL') { | |
return String.fromCharCode(64 + parseInt(m[6].slice(0, 2), 10)) | |
+ m[6].slice(2) + m[12] | |
} else { | |
return m[6] + m[12] | |
} | |
})() : undefined, | |
expiration_date: m[8] ? (() => { | |
switch (m[8]) { | |
case '77': | |
return 'NON-EXPIRING' | |
case '88': | |
return `${m[10]}/${m[7]}` | |
case '99': | |
return `${m[10]}/${m[11]}/m[7]` | |
default: | |
return `${m[8]}/${m[7]}` | |
} | |
})() : undefined, | |
birth_date: m[10] ? (() => { | |
if (m[10] === '99') { | |
m[10] = m[8] | |
} | |
return `${m[10]}/${m[11]}/${m[9]}` | |
})() : undefined, | |
template_version: m[13], | |
security_version: m[14], | |
zip_code: m[15].trim(), | |
class: m[16].trim(), | |
restrictions: m[17].trim(), | |
endorsements: m[18].trim(), | |
sex: sex[m[19].toUpperCase()] || 'UNKNOWN/MISSING', | |
height: m[20].trim(), | |
weight: m[21].trim(), | |
hair_color: m[22].trim(), | |
eyes_color: m[23].trim(), | |
/* | |
id_number: (m[24] || '').trim(), | |
reserved: (m[25] || '').trim(), | |
error_correction: (m[26] || '').trim(), | |
security: (m[27] || '').trim(), | |
*/ | |
} | |
} | |
})() | |
input.addEventListener('input', (event) => { | |
const data = parse(input) | |
if (!data) { | |
output.textContent = 'Incompatible format' | |
} else { | |
output.textContent = ` | |
${data.name} | |
${data.address.replace('\n', '\n\t')} | |
${data.city}, ${data.state} | |
DL#: ${data.dl_id || '-'} | |
Expiration: ${data.expiration_date || '-'} | |
DOB: ${data.birth_date || '-'} | |
IIN: ${data.iin || '-'} | |
ZIP: ${data.zip_code || '-'} | |
Class: ${data.class || '-'} | |
Restrictions: ${data.restrictions || '-'} | |
Endorsements: ${data.endorsements || '-'} | |
Sex: ${data.sex} | |
Height: ${data.height || '-'} | |
Weight: ${data.weight || '-'} | |
Hair color: ${data.hair_color || '-'} | |
Eyes color: ${data.eyes_color || '-'} | |
` | |
} | |
}) | |
document.addEventListener('keydown', (event) => { | |
if (!event.ctrlKey && !event.metaKey) { | |
input.focus() | |
} | |
}) | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment