├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky └── pre-commit ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── __tests__ ├── barcode-dataTest.ts ├── barcode-readerTest.ts ├── block-typesTest.ts ├── checkInputTest.ts ├── check_signatureTest.ts ├── enumsTest.ts ├── get_certsTest.ts ├── helper.ts ├── images │ ├── CT-003.png │ ├── barcode-dummy2.png │ ├── barcode-dummy3.png │ └── no-barcode.png ├── indexTest.ts ├── pdf │ ├── SOURCE │ └── ticketdump.data ├── updateLocalCertsTest.ts └── utilsTest.ts ├── cert_url.json ├── jest.config.ts ├── package-lock.json ├── package.json ├── postinstall ├── updateCerts.d.ts ├── updateCerts.js ├── updateLocalCerts.d.ts └── updateLocalCerts.js ├── src ├── FieldsType.ts ├── TicketContainer.ts ├── barcode-data.ts ├── barcode-reader.ts ├── block-types.ts ├── checkInput.ts ├── check_signature.ts ├── enums.ts ├── get_certs.ts ├── index.ts ├── ka-data.ts ├── postinstall │ ├── updateCerts.ts │ └── updateLocalCerts.ts ├── ticketDataContainers │ ├── TC_0080BL_02.ts │ ├── TC_0080BL_03.ts │ ├── TC_0080ID_01.ts │ ├── TC_0080ID_02.ts │ ├── TC_0080VU_01.ts │ ├── TC_1180AI_01.ts │ ├── TC_U_HEAD_01.ts │ └── TC_U_TLAY_01.ts └── utils.ts ├── tsconfig.json ├── tsconfig.postinstall.json └── tsconfig.release.json /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/eslint-recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier' 8 | ], 9 | parser: '@typescript-eslint/parser', 10 | plugins: ['@typescript-eslint', 'jest', 'prettier'], 11 | root: true, 12 | env: { 13 | browser: false, 14 | es6: true, 15 | node: true 16 | }, 17 | rules: { 18 | '@typescript-eslint/explicit-function-return-type': 1, 19 | 'prettier/prettier': 2 // Means error 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: ['master', 'main'] 9 | pull_request: 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [18, 20, 21] 18 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: 'npm' 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test -- --runInBand 30 | - name: Coveralls 31 | uses: coverallsapp/github-action@v2 32 | with: 33 | flag-name: node-${{ join(matrix.*, '-') }} 34 | parallel: true 35 | 36 | finish: 37 | needs: build 38 | if: ${{ always() }} 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Coveralls Finished 42 | uses: coverallsapp/github-action@v2 43 | with: 44 | parallel-finished: true 45 | carryforward: 'node-18.x,node-20.x,node-21.x' 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CUSTOM 2 | samples/ 3 | keys.json 4 | .DS_Store 5 | build/ 6 | 7 | # https://raw.githubusercontent.com/github/gitignore/master/Node.gitignore 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | .pnpm-debug.log* 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | *.lcov 32 | 33 | # nyc test coverage 34 | .nyc_output 35 | 36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 37 | .grunt 38 | 39 | # Bower dependency directory (https://bower.io/) 40 | bower_components 41 | 42 | # node-waf configuration 43 | .lock-wscript 44 | 45 | # Compiled binary addons (https://nodejs.org/api/addons.html) 46 | build/Release 47 | 48 | # Dependency directories 49 | node_modules/ 50 | jspm_packages/ 51 | 52 | # Snowpack dependency directory (https://snowpack.dev/) 53 | web_modules/ 54 | 55 | # TypeScript cache 56 | *.tsbuildinfo 57 | 58 | # Optional npm cache directory 59 | .npm 60 | 61 | # Optional eslint cache 62 | .eslintcache 63 | 64 | # Optional stylelint cache 65 | .stylelintcache 66 | 67 | # Microbundle cache 68 | .rpt2_cache/ 69 | .rts2_cache_cjs/ 70 | .rts2_cache_es/ 71 | .rts2_cache_umd/ 72 | 73 | # Optional REPL history 74 | .node_repl_history 75 | 76 | # Output of 'npm pack' 77 | *.tgz 78 | 79 | # Yarn Integrity file 80 | .yarn-integrity 81 | 82 | # dotenv environment variable files 83 | .env 84 | .env.development.local 85 | .env.test.local 86 | .env.production.local 87 | .env.local 88 | 89 | # parcel-bundler cache (https://parceljs.org/) 90 | .cache 91 | .parcel-cache 92 | 93 | # Next.js build output 94 | .next 95 | out 96 | 97 | # Nuxt.js build / generate output 98 | .nuxt 99 | dist 100 | 101 | # Gatsby files 102 | .cache/ 103 | # Comment in the public line in if your project uses Gatsby and not Next.js 104 | # https://nextjs.org/blog/next-9-1#public-directory-support 105 | # public 106 | 107 | # vuepress build output 108 | .vuepress/dist 109 | 110 | # vuepress v2.x temp and cache directory 111 | .temp 112 | .cache 113 | 114 | # Docusaurus cache and generated files 115 | .docusaurus 116 | 117 | # Serverless directories 118 | .serverless/ 119 | 120 | # FuseBox cache 121 | .fusebox/ 122 | 123 | # DynamoDB Local files 124 | .dynamodb/ 125 | 126 | # TernJS port file 127 | .tern-port 128 | 129 | # Stores VSCode versions used for testing VSCode extensions 130 | .vscode-test 131 | 132 | # yarn v2 133 | .yarn/cache 134 | .yarn/unplugged 135 | .yarn/build-state.yml 136 | .yarn/install-state.gz 137 | .pnp.* -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run prettier && npm run lint 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .husky 3 | .vscode 4 | .gitignore 5 | src 6 | coverage 7 | node_modules 8 | __tests__ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "none", 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "endOfLine": "lf", 7 | "overrides": [ 8 | { 9 | "files": ["*.ts", "*.mts"], 10 | "options": { 11 | "parser": "typescript" 12 | } 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.eol": "\n", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnPaste": true 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 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 | # uic-918-3.js 2 | 3 | [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](https://www.typescriptlang.org/) 4 | ![Build Status](https://github.com/justusjonas74/uic-918-3/actions/workflows/node.js.yml/badge.svg) 5 | [![Coverage Status](https://coveralls.io/repos/github/justusjonas74/uic-918-3/badge.svg?branch=master)](https://coveralls.io/github/justusjonas74/uic-918-3?branch=master) 6 | [![Maintainability](https://api.codeclimate.com/v1/badges/8a9c146a8fdf552dbbcc/maintainability)](https://codeclimate.com/github/justusjonas74/uic-918-3/maintainability) 7 | [![npm version](https://badge.fury.io/js/uic-918-3.svg)](https://badge.fury.io/js/uic-918-3) 8 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 9 | 10 | A Node.js package written in Typescript for decoding and parsing barcodes according to the "UIC 918.3" specification, which is commonly used on Print and Mobile Tickets from public transport companies (e.g. Deutsche Bahn). 11 | 12 | ## Installation 13 | 14 | To install the latest released version: 15 | 16 | ```bash 17 | npm install uic-918-3 18 | ``` 19 | 20 | Or checkout the master branch on GitHub: 21 | 22 | ```bash 23 | git clone https://github.com/justusjonas74/uic-918-3.git 24 | cd uic-918-3 25 | npm install 26 | ``` 27 | 28 | ## Usage 29 | 30 | ```javascript 31 | import { readBarcode } from 'uic-918-3'; 32 | 33 | // Input could be a string with path to image... 34 | const image = '/path/to/your/file.png'; 35 | // ... or a Buffer object with an image 36 | const image_as_buffer = fs.readFileSync('/path/to/your/file.png'); 37 | 38 | readBarcode('foo.png') 39 | .then((ticket) => console.log(ticket)) 40 | .catch((error) => console.error(error)); 41 | ``` 42 | 43 | ### Options 44 | 45 | Following options are available: 46 | 47 | ```javascript 48 | import { readBarcode } from 'uic-918-3'; 49 | 50 | const image = '/path/to/your/file.png'; 51 | const options = { 52 | verifySignature: true // Verify the signature included in the ticket barcode with a public key set from a Public Key Infrastructure (PKI). The PKI url is set inside './lib/cert_url.json'. Default is 'false'. 53 | }; 54 | 55 | uic.readBarcode(image, options).then((ticket) => { 56 | console.log(ticket.validityOfSignature); // Returns "VALID", "INVALID" or "Public Key not found" 57 | // ticket.isSignatureValid is deprecated. Use validityOfSignature instead. 58 | }); 59 | // 60 | ``` 61 | 62 | ### Returning object 63 | 64 | The returning object consists of (among other things) one or more `TicketDataContainers` which hold ticket data for different purposes. The most interesting containers are: 65 | 66 | - `**U_HEAD**` The ticket header ... 67 | - `**U_TLAY**` A representation of the informations which are printed on the ticket. 68 | - `**0080BL**` A specific container on tickets from Deutsche Bahn. Consists of all relevant information which will be used for proof-of-payment checks on the train. 69 | - `**0080VU**` A specific container on (some) tickets from Deutsche Bahn. This container is used on products, which are also accepted by other carriers, especially (local) public transport companies. Get more information about this container [here](https://www.bahn.de/vdv-barcode). 70 | 71 | ## Optimize your files 72 | 73 | Actually the barcode reader is very dump, so the ticket you want to read, should be optimised before using this package. A better reading logic will be added in future versions. 74 | 75 | ### Images 76 | 77 | Actually the package only supports images with a "nice to read" barcode. So it's best to crop the image to the dimensions of the barcode and save it as a monochrome image (1 bit colour depth). 78 | 79 | ### Extract barcode images from PDF files 80 | 81 | You have to extract the barcode image from your PDF. The fastest (but not the best) way is to make a screen shot and save it as described before. 82 | If you're using Linux or Mac OS X a much better way is to use `poppler-utils` and `imagemagick`: 83 | 84 | ```bash 85 | # Extract images from pdf to .ppm or .pbm images. The last argument is a prefix for the extracted image file names. 86 | pdfimages your-ticket.pdf your-ticket 87 | # convert .ppm/.pbm to a readable format (png) 88 | convert your-ticket-00x.ppm your-ticket-00x.png; 89 | ``` 90 | 91 | ## Expected Quality 92 | 93 | The _UIC 913.3_ specifications aren't available for free, so the whole underlying logic is build upon third party sources, particularly the Python script [onlineticket](https://github.com/rumpeltux/onlineticket/) from Hagen Fritzsch, the [diploma thesis](https://monami.hs-mittweida.de/files/4983/WaitzRoman_Diplomarbeit.pdf) from Roman Waitz and the Wikipedia discussion about [Online-Tickets](https://de.wikipedia.org/wiki/Diskussion:Online-Ticket). Therefore results from this package (especially the parsing logic) should be taken with care. 94 | Please feel free to open an issue, if you guess there's a wrong interpretation of data fields or corresponding values. 95 | 96 | ## Contributing 97 | 98 | Feel free to contribute. 99 | -------------------------------------------------------------------------------- /__tests__/barcode-dataTest.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from '@jest/globals'; 2 | 3 | import { dummyTicket, dummyTicket2 } from './helper'; 4 | 5 | import interpretBarcode, { TicketDataContainer } from '../src/barcode-data'; 6 | import { TicketSignatureVerficationStatus } from '../src/check_signature'; 7 | import { existsSync } from 'fs'; 8 | import { filePath, updateLocalCerts } from '../src/postinstall/updateLocalCerts'; 9 | beforeAll(async () => { 10 | if (!existsSync(filePath)) { 11 | await updateLocalCerts(); 12 | } 13 | }); 14 | describe('barcode-data', () => { 15 | describe('barcode-data.interpret', () => { 16 | test('should return an object for ticket version 1', () => { 17 | const ticket = dummyTicket('U_HEAD', '01', 'Hi!'); 18 | expect(interpretBarcode(ticket)).resolves.toBeInstanceOf(Object); 19 | }); 20 | test('should return an object for ticket version 2', () => { 21 | const ticket = dummyTicket('U_HEAD', '02', 'Hi!'); 22 | expect(interpretBarcode(ticket)).resolves.toBeInstanceOf(Object); 23 | }); 24 | test('should show the correct version for ticket version 1', () => { 25 | const ticket = dummyTicket('U_HEAD', '01', 'Hi!'); 26 | expect(interpretBarcode(ticket)).resolves.toHaveProperty('version', 1); 27 | }); 28 | test('should show the correct version for ticket version 2', () => { 29 | const ticket = dummyTicket2('U_HEAD', '01', 'Hi!'); 30 | expect(interpretBarcode(ticket)).resolves.toHaveProperty('version', 2); 31 | }); 32 | test('should return an empty array if input param is an empty buffer.', async () => { 33 | const emptyTicket = Buffer.from( 34 | '2355543031333431353030303033302e0215008beb83c5db49924a1387e99ed58fe2cc59aa8a8c021500f66f662724ca0b49a95d7f81810cbfa5696d06ed0000', 35 | 'hex' 36 | ); 37 | const ticket = await interpretBarcode(emptyTicket); 38 | expect(Array.isArray(ticket.ticketContainers)).toBe(true); 39 | expect(ticket.ticketContainers).toHaveLength(0); 40 | }); 41 | test('should throw an error if mt_version is not 1 or 2', async () => { 42 | const unsupportedTicket = Buffer.from( 43 | '2355543033333431353030303033302e0215008beb83c5db49924a1387e99ed58fe2cc59aa8a8c021500f66f662724ca0b49a95d7f81810cbfa5696d06ed0000', 44 | 'hex' 45 | ); 46 | try { 47 | await interpretBarcode(unsupportedTicket); 48 | // Fail test if above expression doesn't throw anything. 49 | expect(true).toBe(false); 50 | } catch (e) { 51 | const error: Error = e as Error; 52 | expect(error).toBeInstanceOf(Error); 53 | expect(error.message).toBe( 54 | 'Barcode header contains a version of 3 (instead of 1 or 2), which is not supported by this library yet.' 55 | ); 56 | } 57 | }); 58 | 59 | describe('on unknown data fields', () => { 60 | const ticket = dummyTicket('MYID!!', '01', 'Test'); 61 | test('should ignore unkown data fields', async () => { 62 | const parsedTicket = await interpretBarcode(ticket); 63 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 64 | expect(Object.keys(results)).not.toHaveLength(0); 65 | }); 66 | test('should parse the unknown container id', async () => { 67 | const parsedTicket = await interpretBarcode(ticket); 68 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 69 | expect(results[0].id).toBe('MYID!!'); 70 | }); 71 | test('should not touch/parse the container data', async () => { 72 | const parsedTicket = await interpretBarcode(ticket); 73 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 74 | expect(results[0].container_data).toEqual(Buffer.from('Test')); 75 | }); 76 | }); 77 | describe('on unknown data fieds versions but known id', () => { 78 | const ticket = dummyTicket('U_HEAD', '03', 'Test'); 79 | test('should ignore unkown versions of data fields', async () => { 80 | const parsedTicket = await interpretBarcode(ticket); 81 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 82 | expect(Object.keys(results)).not.toHaveLength(0); 83 | }); 84 | test('should parse the unknown container id', async () => { 85 | const parsedTicket = await interpretBarcode(ticket); 86 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 87 | expect(results[0].id).toBe('U_HEAD'); 88 | }); 89 | test('should not touch/parse the container data', async () => { 90 | const parsedTicket = await interpretBarcode(ticket); 91 | const results: TicketDataContainer[] = parsedTicket.ticketContainers as TicketDataContainer[]; 92 | expect(results[0].container_data).toEqual(Buffer.from('Test')); 93 | }); 94 | }); 95 | describe('verify Signature', () => { 96 | test('should recognize an invalid signature', async () => { 97 | const invalidTicket = Buffer.from( 98 | '2355543031313038303030303036302e0215008beb83c5db4992412387e99ed58fe2cc59aa8a8c021500f66f662724ca0b49a95d7f81810cbfa5696d06ed000030313730789c4d4ec10a824014fc95f70125f376db6cbd098a1db28434e8144b6c21a50777fdff56106d18dee1cd0c33cde398a719185052ee58710cc54ad13fa0a10438660eb62c276a1ef529bd87106bce2fd706218d09c133dd436906dff686cad1793b74a6efc1abbcafcddbbaba7d7eaca7ea3b3a884514ba1a6ceb9c1f5f96181bba15672aac339d1fccd8412e4e29451c4145d3320212602bf4fa90c9bc6a2e0d8c02596bfc005e033b47', 99 | 'hex' 100 | ); 101 | const barcode = await interpretBarcode(invalidTicket, true); 102 | expect(barcode).toHaveProperty('isSignatureValid', false); 103 | expect(barcode).toHaveProperty('validityOfSignature', TicketSignatureVerficationStatus.INVALID); 104 | }); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /__tests__/barcode-readerTest.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { ZXing } from '../src/barcode-reader'; 3 | 4 | import { describe, expect, test } from '@jest/globals'; 5 | 6 | describe('barcode-reader.js', () => { 7 | describe('barcode-reader.ZXing', function () { 8 | const dummy = readFileSync('__tests__/images/barcode-dummy2.png'); 9 | const ticket = Buffer.from( 10 | '2355543031333431353030303033302e0215008beb83c5db49924a1387e99ed58fe2cc59aa8a8c021500f66f662724ca0b49a95d7f81810cbfa5696d06ed000030313730789c4d4ec10a824014fc95f70125f376db6cbd098a1db28434e8144b6c21a50777fdff56106d18dee1cd0c33cde398a719185052ee58710cc54ad13fa0a10438660eb62c276a1ef529bd87106bce2fd706218d09c133dd436906dff686cad1793b74a6efc1abbcafcddbbaba7d7eaca7ea3b3a884514ba1a6ceb9c1f5f96181bba15672aac339d1fccd8412e4e29451c4145d3320212602bf4fa90c9bc6a2e0d8c02596bfc005e033b47', 11 | 'hex' 12 | ); 13 | test('should return an object on sucess', () => { 14 | return expect(ZXing(dummy)).resolves.toBeInstanceOf(Buffer); 15 | }); 16 | test('should return the ticket data', () => { 17 | return expect(ZXing(dummy)).resolves.toEqual(ticket); 18 | }); 19 | test('should throw an error, if no barcode is found', () => { 20 | const noBarcodeImage = readFileSync('./__tests__/images/no-barcode.png'); 21 | expect(ZXing(noBarcodeImage)).rejects.toThrowError('Could not detect a valid Aztec barcode'); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /__tests__/block-typesTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeAll } from '@jest/globals'; 2 | import bt from '../src/TicketContainer'; 3 | import { interpretFieldResult } from '../src/utils'; 4 | import { IEFS_DATA, RCT2_BLOCK } from '../src/block-types'; 5 | 6 | describe('block-types.js', () => { 7 | test('should return an array', () => { 8 | expect(Array.isArray(bt)).toBe(true); 9 | }); 10 | test('should only return objects inside the array with a property of name', () => { 11 | bt.forEach((blockType) => { 12 | expect(blockType).toHaveProperty('name'); 13 | }); 14 | }); 15 | test('should only return objects inside the array with a property of versions', () => { 16 | bt.forEach((blockType) => { 17 | expect(blockType).toHaveProperty('version'); 18 | }); 19 | }); 20 | describe('Generic Types', () => { 21 | describe('STRING', () => { 22 | test('should return an String from a Buffer', () => { 23 | // const dataTypeArr = bt[0].versions['01'][0] 24 | const dataTypeArr = bt.find((container) => container.name == 'U_HEAD' && container.version == '01') 25 | ?.dataFields[0]; 26 | const res = dataTypeArr?.interpreterFn; 27 | const testValue = 'GAUF'; 28 | expect(res!(Buffer.from(testValue))).toBe(testValue); 29 | }); 30 | }); 31 | describe('HEX', () => { 32 | test('should return a hexadecimal encoded string representation from a Buffer', () => { 33 | const dataTypeArr = bt.find((container) => container.name == 'U_HEAD' && container.version == '01') 34 | ?.dataFields[2]; 35 | const res = dataTypeArr?.interpreterFn; 36 | const testValue = '0123456789abcdef'; 37 | expect(res!(Buffer.from(testValue, 'hex'))).toBe(testValue); 38 | }); 39 | }); 40 | describe('STR_INT', () => { 41 | test('should return a number from a Buffer encoded string', () => { 42 | const dataTypeArr = bt.find((container) => container.name == 'U_TLAY' && container.version == '01') 43 | ?.dataFields[1]; 44 | const res = dataTypeArr?.interpreterFn; 45 | const testValue = 1234; 46 | const testValueBuf = Buffer.from(testValue.toString(10)); 47 | expect(res!(testValueBuf)).toBe(testValue); 48 | }); 49 | }); 50 | describe('DB_DATETIME', () => { 51 | test('should return a Date from a Buffer encoded string', () => { 52 | const dataTypeArr = bt.find((container) => container.name == 'U_HEAD' && container.version == '01') 53 | ?.dataFields[3]; 54 | const res = dataTypeArr?.interpreterFn; 55 | const str = '130419871215'; 56 | const dummyDateBuf = Buffer.from(str); 57 | const dummyDate = new Date(1987, 3, 13, 12, 15); 58 | expect(res!(dummyDateBuf)).toEqual(dummyDate); 59 | }); 60 | }); 61 | }); 62 | describe('Special Types', () => { 63 | describe('EFS_DATA', () => { 64 | let res: IEFS_DATA; 65 | let res2: IEFS_DATA; 66 | let resDc10: IEFS_DATA; 67 | beforeAll((done) => { 68 | const fn = bt.find((container) => container.name == '0080VU' && container.version == '01')?.dataFields[4] 69 | .interpreterFn; 70 | const testBuf = Buffer.from('130791f0187407d018763821000138221800000000130791f008dc060d18767a131c', 'hex'); 71 | const testBufDc10 = Buffer.from('130791f0187407d018763821000138221800000000130791f008dc061018767a131c', 'hex'); 72 | const doubleTestBuf = Buffer.from( 73 | '130791f0187407d018763821000138221800000000130791f008dc060d18767a131c130791f0187407d018763821000138221800000000130791f008dc060d18767a131c', 74 | 'hex' 75 | ); 76 | res = fn!(testBuf) as IEFS_DATA; 77 | res2 = fn!(doubleTestBuf) as IEFS_DATA; 78 | resDc10 = fn!(testBufDc10) as IEFS_DATA; 79 | done(); 80 | }); 81 | test('should return an object', () => { 82 | expect(res).toBeInstanceOf(Object); 83 | }); 84 | test('should have property of property of berechtigungs_nr', () => { 85 | expect(res['1']).toHaveProperty('berechtigungs_nr', 319263216); 86 | }); 87 | 88 | test('should also work with mutliple tickets', () => { 89 | expect(res2['1']).toHaveProperty('berechtigungs_nr', 319263216); 90 | }); 91 | test('should handle DC-typ 0x0d', () => { 92 | expect(res['1']).toHaveProperty('Liste_DC.typ_DC', '0d'); 93 | }); 94 | test('should handle DC-typ 0x10', () => { 95 | expect(resDc10['1']).toHaveProperty('Liste_DC.typ_DC', '10'); 96 | }); 97 | test('should parse the DateTime correctly', () => { 98 | expect(res['1']).toHaveProperty('valid_from', new Date(2018, 0, 1, 0, 0, 0)); // .and.be.instanceof(Date); 99 | }); 100 | }); 101 | describe('RCT2_TEST_DATA', () => { 102 | const fn = bt.find((container) => container.name == 'U_TLAY' && container.version == '01')?.dataFields[2] 103 | .interpreterFn; 104 | const RCT2_TEST_DATA = Buffer.from( 105 | '303030303031373130303031384d617274696e61204d75737465726d616e6e3031303030313731303030313654616765735469636b657420506c757330323030303137313030303239507265697373747566652031302c2056474e20476573616d747261756d3033303030313731303030333332372e30352e323031372030303a30302d32392e30352e323031372030333a30303035303030313731303030313030312e30312e31393930', 106 | 'hex' 107 | ); 108 | const result = fn!(RCT2_TEST_DATA) as RCT2_BLOCK[]; 109 | test('should return an array', () => { 110 | expect(Array.isArray(result)).toBe(true); 111 | }); 112 | test('should return object as array items', () => { 113 | result.forEach((items) => expect(items).toBeInstanceOf(Object)); 114 | }); 115 | test('should return objects inside array with specific properties', () => { 116 | result.forEach((item) => { 117 | expect(item).toHaveProperty('line'); 118 | expect(item).toHaveProperty('column'); 119 | expect(item).toHaveProperty('height'); 120 | expect(item).toHaveProperty('width'); 121 | expect(item).toHaveProperty('style'); 122 | expect(item).toHaveProperty('value'); 123 | }); 124 | }); 125 | test('should parse the content of properties correctly', () => { 126 | expect(result[0]).toHaveProperty('line', 0); 127 | expect(result[0]).toHaveProperty('column', 0); 128 | expect(result[0]).toHaveProperty('height', 1); 129 | expect(result[0]).toHaveProperty('width', 71); 130 | expect(result[0]).toHaveProperty('style', 0); 131 | expect(result[0]).toHaveProperty('value', 'Martina Mustermann'); 132 | }); 133 | }); 134 | describe('auftraegeSblocks_V3', () => { 135 | const fn = bt.find((container) => container.name == '0080BL' && container.version == '03')?.dataFields[1] 136 | .interpreterFn; 137 | const TEST_DATA = Buffer.from( 138 | '313031303132303138303130313230313832373839343134353200313653303031303030395370617270726569735330303230303031325330303330303031415330303930303036312d312d3439533031323030303130533031343030303253325330313530303035526965736153303136303031344ec3bc726e626572672b4369747953303231303033304e562a4c2d4862662031353a343820494345313531332d494345313731335330323330303133446f656765204672616e6369735330323630303032313353303238303031334672616e63697323446f656765533033313030313030312e30312e32303138533033323030313030312e30312e32303138533033353030303531303239375330333630303033323834', 139 | 'hex' 140 | ); 141 | const result = fn!(TEST_DATA); 142 | test('should return an object', () => { 143 | expect(result).toBeInstanceOf(Object); 144 | }); 145 | test('should return an object with correct properties', () => { 146 | expect(result).toHaveProperty('auftrag_count', 1); 147 | expect(result).toHaveProperty('sblock_amount', 16); 148 | expect(result).toHaveProperty('auftrag_1', { 149 | valid_from: '01012018', 150 | valid_to: '01012018', 151 | serial: '278941452\u0000' 152 | }); 153 | }); 154 | describe('auftraegeSblocks_V3.sblocks', () => { 155 | const fn = bt.find((container) => container.name == '0080BL' && container.version == '03')?.dataFields[1] 156 | .interpreterFn; 157 | const TEST_DATA = Buffer.from( 158 | '313031303132303138303130313230313832373839343134353200313653303031303030395370617270726569735330303230303031325330303330303031415330303930303036312d312d3439533031323030303130533031343030303253325330313530303035526965736153303136303031344ec3bc726e626572672b4369747953303231303033304e562a4c2d4862662031353a343820494345313531332d494345313731335330323330303133446f656765204672616e6369735330323630303032313353303238303031334672616e63697323446f656765533033313030313030312e30312e32303138533033323030313030312e30312e32303138533033353030303531303239375330333630303033323834', 159 | 'hex' 160 | ); 161 | const result = fn!(TEST_DATA) as interpretFieldResult; 162 | test('should return an object', () => { 163 | expect(result.sblocks).toBeInstanceOf(Object); 164 | }); 165 | }); 166 | }); 167 | describe('auftraegeSblocks_V2', () => { 168 | const fn = bt.find((container) => container.name == '0080BL' && container.version == '02')?.dataFields[1] 169 | .interpreterFn; 170 | const TEST_DATA = Buffer.from( 171 | '3130313233343536373839213031323334353637383921303130343230313830313034323031383031303432303138313653303031303030395370617270726569735330303230303031325330303330303031415330303930303036312d312d3439533031323030303130533031343030303253325330313530303035526965736153303136303031344ec3bc726e626572672b4369747953303231303033304e562a4c2d4862662031353a343820494345313531332d494345313731335330323330303133446f656765204672616e6369735330323630303032313353303238303031334672616e63697323446f656765533033313030313030312e30312e32303138533033323030313030312e30312e32303138533033353030303531303239375330333630303033323834', 172 | 'hex' 173 | ); 174 | const result = fn!(TEST_DATA); 175 | test('should return an object', () => { 176 | expect(result).toBeInstanceOf(Object); 177 | }); 178 | test('should return an object with correct properties', () => { 179 | expect(result).toHaveProperty('auftrag_count', 1); 180 | expect(result).toHaveProperty('sblock_amount', 16); 181 | expect(result).toHaveProperty('auftrag_1', { 182 | certificate: '0123456789!', 183 | padding: '3031323334353637383921', 184 | valid_from: '01042018', 185 | valid_to: '01042018', 186 | serial: '01042018' 187 | }); 188 | }); 189 | }); 190 | describe('AUSWEIS_TYP', () => { 191 | const fn = bt.find((container) => container.name == '0080ID' && container.version == '01')?.dataFields[0] 192 | .interpreterFn; 193 | const TEST_DATA = Buffer.from('09'); 194 | const result = fn!(TEST_DATA); 195 | test('should return a string', () => { 196 | expect(typeof result).toBe('string'); 197 | }); 198 | test('should parse the value correctly', () => { 199 | expect(result).toBe('Personalausweis'); 200 | }); 201 | }); 202 | }); 203 | }); 204 | -------------------------------------------------------------------------------- /__tests__/checkInputTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test, beforeAll } from '@jest/globals'; 2 | 3 | import path from 'path'; 4 | import fs from 'fs'; 5 | 6 | import { loadFileOrBuffer } from '../src/checkInput'; 7 | 8 | describe('checkInput.js', () => { 9 | const filePath = { 10 | relative_true: '', 11 | relative_false: '' + '1458', 12 | absolute_true: '', 13 | absolute_false: '' 14 | }; 15 | 16 | beforeAll(() => { 17 | const file = 'package.json'; 18 | filePath.relative_true = file; 19 | filePath.relative_false = file + '1458'; 20 | filePath.absolute_true = path.resolve(file); 21 | filePath.absolute_false = path.resolve(file) + '254'; 22 | }); 23 | 24 | describe('loadFileOrBuffer', () => { 25 | describe('with no optional parameters - relative filepath', () => { 26 | test('should be fulfilled with a string', () => { 27 | return expect(loadFileOrBuffer(filePath.relative_true)).resolves.toStrictEqual( 28 | fs.readFileSync(filePath.relative_true) 29 | ); 30 | }); 31 | test('should be fulfilled with a string - absolute filepath', () => { 32 | return expect(loadFileOrBuffer(filePath.absolute_true)).resolves.toStrictEqual( 33 | fs.readFileSync(filePath.absolute_true) 34 | ); 35 | }); 36 | test('should be fulfilled with a Buffer', () => { 37 | const buf = Buffer.from('01125684'); 38 | 39 | return expect(loadFileOrBuffer(buf)).resolves.toBe(buf); 40 | }); 41 | test('should be rejected with a wrong file path', () => { 42 | return expect(loadFileOrBuffer(filePath.relative_false)).rejects.toThrow(); 43 | }); 44 | test('should throw an error, if argument is not a Buffer or string', () => { 45 | // @ts-expect-error Testing correct Error Handling in a non typesafe JS runtime 46 | return expect(loadFileOrBuffer(123)).rejects.toThrow(); 47 | }); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/check_signatureTest.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from '@jest/globals'; 2 | 3 | import { TicketSignatureVerficationStatus, verifyTicket } from '../src/check_signature'; 4 | import { ParsedUIC918Barcode, TicketDataContainer } from '../src/barcode-data'; 5 | import { updateLocalCerts, filePath } from '../src/postinstall/updateLocalCerts'; 6 | import { existsSync } from 'node:fs'; 7 | beforeAll(async () => { 8 | if (!existsSync(filePath)) { 9 | await updateLocalCerts(); 10 | } 11 | }); 12 | 13 | describe('check_signature.js', () => { 14 | describe('verifyTicket()', () => { 15 | test('should return VALID if a valid signature is given', () => { 16 | const ticket: ParsedUIC918Barcode = { 17 | signature: Buffer.from( 18 | '302c02146b646f806c2cbc1f16977166e626c3a251c30b5602144917f4e606dfa8150eb2fa4c174378972623e47400000000', 19 | 'hex' 20 | ), 21 | ticketDataRaw: Buffer.from( 22 | '789c6d90cd4ec24010c78b07f5e2c5534f86c48350539cd98f523c9014ba056285c40ae1661a056c2484b495a8275fc877f1017c1867900307770ffbdfdffee76367fcd037410808a025800f919a7ad3c095d6de124d04411ba5d2109ad0b0b1138304891a04a204147caabf532bbfa93ca5b5855e029c1b5ad172f6b6ce6759414010404142b20848b4486874c1858453700c0945422464a42a80789316c56c79d9cdca77421ee789f274f5327fcdcbda6d9aadeabb374115154e06c175b5371ede3bb58ee9387d73973851e8f44c3cbcea8e4cecc4a338767a833a05c86d438fcf79362fab715a94c43caece6d0a9f5f999eef2c097d9c7b44d9006cf09789882d517b84ba06c59c3467a320cda39b8c79267ed37aa2e1560e2ebe6a73bb3cfab6376dd7aab41b36cf9ce1f1cfe189bdf938fba4cbe23fc762e738cd7e01b9e06a43', 23 | 'hex' 24 | ), 25 | header: { 26 | umid: Buffer.from('235554', 'hex'), 27 | mt_version: Buffer.from('3031', 'hex'), 28 | rics: Buffer.from('30303830', 'hex'), 29 | key_id: Buffer.from('3030303037', 'hex') 30 | }, 31 | version: 1, 32 | ticketContainers: [], 33 | ticketDataLength: Buffer.from(''), 34 | ticketDataUncompressed: Buffer.from('') 35 | }; 36 | return expect(verifyTicket(ticket)).resolves.toBe(TicketSignatureVerficationStatus.VALID); 37 | }); 38 | test('should return INVALID if an invalid message is given', () => { 39 | const ticket = { 40 | signature: Buffer.from( 41 | '302c02146b646f806c2cbc1f16977166e626c3a251c30b5602144917f4e606dfa8150eb2fa4c174378972623e47400000000', 42 | 'hex' 43 | ), 44 | ticketDataRaw: Buffer.from('f000', 'hex'), 45 | header: { 46 | umid: Buffer.from('235554', 'hex'), 47 | mt_version: Buffer.from('3031', 'hex'), 48 | rics: Buffer.from('30303830', 'hex'), 49 | key_id: Buffer.from('3030303037', 'hex') 50 | }, 51 | version: 1, 52 | ticketContainers: [] as TicketDataContainer[], 53 | ticketDataLength: Buffer.from(''), 54 | ticketDataUncompressed: Buffer.from('') 55 | }; 56 | return expect(verifyTicket(ticket)).resolves.toBe(TicketSignatureVerficationStatus.INVALID); 57 | }); 58 | test("should return NOPUBLICKEY if a valid signature is given, but the public key isn't found", () => { 59 | const ticket: ParsedUIC918Barcode = { 60 | signature: Buffer.from( 61 | '302c02146b646f806c2cbc1f16977166e626c3a251c30b5602144917f4e606dfa8150eb2fa4c174378972623e47400000000', 62 | 'hex' 63 | ), 64 | ticketDataRaw: Buffer.from( 65 | '789c6d90cd4ec24010c78b07f5e2c5534f86c48350539cd98f523c9014ba056285c40ae1661a056c2484b495a8275fc877f1017c1867900307770ffbdfdffee76367fcd037410808a025800f919a7ad3c095d6de124d04411ba5d2109ad0b0b1138304891a04a204147caabf532bbfa93ca5b5855e029c1b5ad172f6b6ce6759414010404142b20848b4486874c1858453700c0945422464a42a80789316c56c79d9cdca77421ee789f274f5327fcdcbda6d9aadeabb374115154e06c175b5371ede3bb58ee9387d73973851e8f44c3cbcea8e4cecc4a338767a833a05c86d438fcf79362fab715a94c43caece6d0a9f5f999eef2c097d9c7b44d9006cf09789882d517b84ba06c59c3467a320cda39b8c79267ed37aa2e1560e2ebe6a73bb3cfab6376dd7aab41b36cf9ce1f1cfe189bdf938fba4cbe23fc762e738cd7e01b9e06a43', 66 | 'hex' 67 | ), 68 | header: { 69 | umid: Buffer.from('235554', 'hex'), 70 | mt_version: Buffer.from('3031', 'hex'), 71 | rics: Buffer.from('38383838', 'hex'), // Not existing RICS CODE 72 | key_id: Buffer.from('3838383838', 'hex') 73 | }, 74 | version: 1, 75 | ticketContainers: [], 76 | ticketDataLength: Buffer.from(''), 77 | ticketDataUncompressed: Buffer.from('') 78 | }; 79 | return expect(verifyTicket(ticket)).resolves.toBe(TicketSignatureVerficationStatus.NOPUBLICKEY); 80 | }); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /__tests__/enumsTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from '@jest/globals'; 2 | 3 | import { efm_produkt, id_types, sBlockTypes, orgid, tarifpunkt } from '../src/enums'; 4 | 5 | describe('enums.sBlockTypes', () => { 6 | test('should return an instance of enum', () => { 7 | expect(sBlockTypes).toBeInstanceOf(Object); 8 | }); 9 | }); 10 | describe('id_types', () => { 11 | const result = id_types; 12 | test('should return an instance of enum', () => { 13 | expect(result).toBeInstanceOf(Object); 14 | }); 15 | }); 16 | describe('enums.efm_produkt', () => { 17 | test('should return a object', () => { 18 | expect(efm_produkt(6263, 1005)).toBeInstanceOf(Object); 19 | }); 20 | test('should have correct property kvp_organisations_id', () => { 21 | expect(efm_produkt(6263, 1005)).toHaveProperty('kvp_organisations_id', '6263 (DB Regio Zentrale)'); 22 | }); 23 | test('should have correct property produkt_nr', () => { 24 | expect(efm_produkt(6263, 1005)).toHaveProperty('produkt_nr', '1005 (Bayern-Ticket)'); 25 | }); 26 | test('should ignore unknow products', () => { 27 | expect(efm_produkt(6263, 1)).toHaveProperty('kvp_organisations_id', '6263 (DB Regio Zentrale)'); 28 | expect(efm_produkt(6263, 1)).toHaveProperty('produkt_nr', '1'); 29 | }); 30 | test('should ignore unknow organisations', () => { 31 | expect(efm_produkt(815, 1005)).toHaveProperty('kvp_organisations_id', '815'); 32 | expect(efm_produkt(815, 1005)).toHaveProperty('produkt_nr', '1005'); 33 | }); 34 | }); 35 | 36 | describe('enums.org_id', () => { 37 | test('should return a string with the correct value', () => { 38 | expect(typeof orgid(6262)).toBe('string'); 39 | }); 40 | test('should ignore unknown values', () => { 41 | expect(typeof orgid(815)).toBe('string'); 42 | }); 43 | }); 44 | 45 | describe('enums.tarifpunkt', () => { 46 | test('should return a string', () => { 47 | expect(typeof tarifpunkt(6263, 8000284)).toBe('string'); 48 | }); 49 | test('should have correct properties', () => { 50 | expect(tarifpunkt(6263, 8000284)).toBe('8000284 (Nürnberg Hbf)'); 51 | }); 52 | test('should ignore unknow stops', () => { 53 | expect(tarifpunkt(6263, 1)).toBe('1'); 54 | }); 55 | test('should ignore unknow organisations', () => { 56 | expect(tarifpunkt(1, 1)).toBe('1'); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /__tests__/get_certsTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, beforeAll } from '@jest/globals'; 2 | import { getCertByID } from '../src/get_certs'; 3 | import { existsSync } from 'fs'; 4 | import { filePath, updateLocalCerts } from '../src/postinstall/updateLocalCerts'; 5 | 6 | describe('get_certs.js', () => { 7 | beforeAll(async () => { 8 | if (!existsSync(filePath)) { 9 | await updateLocalCerts(); 10 | } 11 | }); 12 | describe('getCertByID', () => { 13 | test('should return a certificate if key is found', async () => { 14 | await expect(getCertByID(1080, 6)).resolves.toBeInstanceOf(Object); 15 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('issuerName'); 16 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('issuerCode'); 17 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('versionType'); 18 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('signatureAlgorithm'); 19 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('id'); 20 | await expect(getCertByID(1080, 6)).resolves.toHaveProperty('publicKey'); 21 | }); 22 | test('should return undefined if key not found', async () => { 23 | await expect(getCertByID(1, 1)).resolves.toBeUndefined(); 24 | }); 25 | test('should return undefined if key file not found', async () => { 26 | await expect(getCertByID(1, 1, 'foot.txt')).resolves.toBeUndefined(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /__tests__/helper.ts: -------------------------------------------------------------------------------- 1 | import { deflateSync } from 'zlib'; 2 | 3 | function pad(num: string | number, size: number): string { 4 | let s = num + ''; 5 | while (s.length < size) s = '0' + s; 6 | return s; 7 | } 8 | 9 | export const dummyTicket = (idStr: string, version: string, bodyStr: string): Buffer => { 10 | const ticketHeader = Buffer.from( 11 | '2355543031303038303030303036302c021402a7689c8181e5c32b839b21f603972512d26504021441b789b47ea70c02ae1b8106d3362ad1cd34de5b00000000', 12 | 'hex' 13 | ); 14 | const dataLengthStr = pad(bodyStr.length + 12, 4); 15 | const senslessContainer = Buffer.from(idStr + version + dataLengthStr + bodyStr); 16 | const compressedTicket = deflateSync(senslessContainer); 17 | const senslessContainerLength = Buffer.from(pad(compressedTicket.length, 4)); 18 | const ticketArr = [ticketHeader, senslessContainerLength, compressedTicket]; 19 | const totalLength = ticketArr.reduce((result, item) => result + item.length, 0); 20 | return Buffer.concat(ticketArr, totalLength); 21 | }; 22 | 23 | export const dummyTicket2 = (idStr: string, version: string, bodyStr: string): Buffer => { 24 | const ticketHeader = Buffer.from( 25 | '2355543032313038303030303032782e2fe184a1d85e89e9338b298ec61aeba248ce722056ca940a967c8a1d39126e2c628c4fcea91ba35216a0a350f894de5ebd7b8909920fde947feede0e20c430313939789c01bc0043ff555f464c455831333031383862b20086e10dc125ea2815110881051c844464d985668e23a00a80000e96c2e4e6e8cadc08aed2d8d9010444d7be0100221ce610ea559b64364c38a82361d1cb5e1e5d32a3d0979bd099c8426b0b7373432b4b6852932baba3634b733b2b715ab34b09d101e18981c181f1424221521291521292a17a3a920a11525a095282314952b20a49529952826278083001a4c38ae5bb303ace700380070014b00240400f537570657220537061727072656973c41e4a03', 26 | 'hex' 27 | ); 28 | const dataLengthStr = pad(bodyStr.length + 12, 4); 29 | const senslessContainer = Buffer.from(idStr + version + dataLengthStr + bodyStr); 30 | const compressedTicket = deflateSync(senslessContainer); 31 | const senslessContainerLength = Buffer.from(pad(compressedTicket.length, 4)); 32 | const ticketArr = [ticketHeader, senslessContainerLength, compressedTicket]; 33 | const totalLength = ticketArr.reduce((result, item) => result + item.length, 0); 34 | return Buffer.concat(ticketArr, totalLength); 35 | }; 36 | -------------------------------------------------------------------------------- /__tests__/images/CT-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusjonas74/uic-918-3/5c3ad2520387e1ad3ccc9735f86fde55903c3b39/__tests__/images/CT-003.png -------------------------------------------------------------------------------- /__tests__/images/barcode-dummy2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusjonas74/uic-918-3/5c3ad2520387e1ad3ccc9735f86fde55903c3b39/__tests__/images/barcode-dummy2.png -------------------------------------------------------------------------------- /__tests__/images/barcode-dummy3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusjonas74/uic-918-3/5c3ad2520387e1ad3ccc9735f86fde55903c3b39/__tests__/images/barcode-dummy3.png -------------------------------------------------------------------------------- /__tests__/images/no-barcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusjonas74/uic-918-3/5c3ad2520387e1ad3ccc9735f86fde55903c3b39/__tests__/images/no-barcode.png -------------------------------------------------------------------------------- /__tests__/indexTest.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from '@jest/globals'; 2 | 3 | import fs, { existsSync } from 'fs'; 4 | import { readBarcode } from '../src/index'; 5 | import { TicketSignatureVerficationStatus } from '../src/check_signature'; 6 | import { filePath, updateLocalCerts } from '../src/postinstall/updateLocalCerts'; 7 | beforeAll(async () => { 8 | if (!existsSync(filePath)) { 9 | await updateLocalCerts(); 10 | } 11 | }); 12 | 13 | describe('index.js', () => { 14 | describe('index.readBarcode', () => { 15 | describe('...when inputis a local file', () => { 16 | const dummy = '__tests__/images/barcode-dummy2.png'; 17 | // const dummy3 = '__tests__/images/barcode-dummy3.png' 18 | const dummy4 = '__tests__/images/CT-003.png'; 19 | 20 | const falseDummy = '__tests__/images/barcode dummy.png'; 21 | test('should return an object on sucess', () => { 22 | return expect(readBarcode(dummy)).toBeInstanceOf(Object); 23 | }); 24 | test('should eventually be resolved', () => { 25 | return expect(readBarcode(dummy)).resolves.toBeTruthy(); 26 | }); 27 | test('should reject if file not found', () => { 28 | return expect(readBarcode(falseDummy)).rejects.toThrow(); 29 | }); 30 | test('should handle verifySignature option and resolve', async () => { 31 | return expect(readBarcode(dummy4, { verifySignature: true })).resolves.toHaveProperty('isSignatureValid'); 32 | }); 33 | }); 34 | describe('...when input is an image buffer', () => { 35 | const dummyBuff = fs.readFileSync('__tests__/images/barcode-dummy2.png'); 36 | const dummy4Buff = fs.readFileSync('__tests__/images/CT-003.png'); 37 | test('should return an object on sucess', async () => { 38 | const barcode = await readBarcode(dummyBuff); 39 | return expect(barcode).toBeInstanceOf(Object); 40 | }); 41 | 42 | test('should handle verifySignature option and resolve on valid tickets', async () => { 43 | const barcode = await readBarcode(dummy4Buff, { verifySignature: true }); 44 | expect(barcode).toHaveProperty('isSignatureValid', true); 45 | expect(barcode).toHaveProperty('validityOfSignature', TicketSignatureVerficationStatus.VALID); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /__tests__/pdf/SOURCE: -------------------------------------------------------------------------------- 1 | https://www.bahn.de/p/view/angebot/regio/barcode.shtml 2 | -------------------------------------------------------------------------------- /__tests__/pdf/ticketdump.data: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justusjonas74/uic-918-3/5c3ad2520387e1ad3ccc9735f86fde55903c3b39/__tests__/pdf/ticketdump.data -------------------------------------------------------------------------------- /__tests__/updateLocalCertsTest.ts: -------------------------------------------------------------------------------- 1 | import { existsSync, unlinkSync, readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { describe, test, beforeAll, expect } from '@jest/globals'; 4 | import { fileName } from '../cert_url.json'; 5 | 6 | import { updateLocalCerts } from '../src/postinstall/updateLocalCerts'; 7 | 8 | const filePath = join(__dirname, '../', fileName); 9 | 10 | describe('updateLocalCerts', () => { 11 | beforeAll(() => { 12 | if (existsSync(filePath)) { 13 | unlinkSync(filePath); 14 | } 15 | }); 16 | test('should create a not empty file', async () => { 17 | await updateLocalCerts(); 18 | expect(existsSync(filePath)).toBeTruthy(); 19 | expect(readFileSync(filePath).length).toBeGreaterThan(0); 20 | }, 120000); 21 | }); 22 | -------------------------------------------------------------------------------- /__tests__/utilsTest.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, beforeEach, test, jest } from '@jest/globals'; 2 | 3 | import { handleError, interpretField, pad, parseContainers, parsingFunction } from '../src/utils'; 4 | import { FieldsType, SupportedTypes } from '../src/FieldsType'; 5 | 6 | describe('utils.js', () => { 7 | describe('utils.interpretField', () => { 8 | test('should return an object', () => { 9 | const data = Buffer.from('Test'); 10 | const fields: FieldsType[] = []; 11 | const result = interpretField(data, fields); 12 | expect(result).toBeInstanceOf(Object); 13 | }); 14 | test('should return an empty object if fields is an empty arry', () => { 15 | const data = Buffer.from('Test'); 16 | const fields: FieldsType[] = []; 17 | const result = interpretField(data, fields); 18 | expect(Object.keys(result)).toHaveLength(0); 19 | }); 20 | test('should parse a buffer using a given data field specification', () => { 21 | const data = Buffer.from([0x14, 0x14, 0x06, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]); 22 | const fields: FieldsType[] = [ 23 | { 24 | name: 'TAG', 25 | length: 2, 26 | interpreterFn: (x) => x.toString('hex') 27 | }, 28 | { 29 | name: 'LENGTH', 30 | length: 1 31 | }, 32 | { 33 | name: 'TEXT', 34 | length: undefined, 35 | interpreterFn: (x) => x.toString() 36 | } 37 | ]; 38 | const result = interpretField(data, fields); 39 | expect(result.TAG).toBe('1414'); 40 | expect(result.LENGTH).toEqual(Buffer.from('06', 'hex')); 41 | expect(result.TEXT).toBe('Hello!'); 42 | }); 43 | test('should parse a buffer using a given data field specification', () => { 44 | const data = Buffer.from([0x14, 0x14, 0x06, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]); 45 | const fields: FieldsType[] = [ 46 | { 47 | name: 'TAG', 48 | length: 2, 49 | interpreterFn: (x: Buffer) => x.toString('hex') 50 | }, 51 | { 52 | name: 'LENGTH', 53 | length: 1 54 | }, 55 | { 56 | name: 'TEXT', 57 | length: undefined, 58 | interpreterFn: (x: Buffer) => x.toString() 59 | } 60 | ]; 61 | const result = interpretField(data, fields); 62 | expect(result.TAG).toBe('1414'); 63 | expect(result.LENGTH).toEqual(Buffer.from('06', 'hex')); 64 | expect(result.TEXT).toBe('Hello!'); 65 | }); 66 | }); 67 | 68 | describe('utils.parseContainers', () => { 69 | let results: SupportedTypes[]; 70 | beforeEach((done) => { 71 | const data = Buffer.from('Test'); 72 | const parsingFunction: parsingFunction = (buf: Buffer) => { 73 | const firstElement = buf.subarray(0, 1).toString(); 74 | const secondElement = buf.subarray(1); 75 | return [firstElement, secondElement]; 76 | }; 77 | results = parseContainers(data, parsingFunction); 78 | done(); 79 | }); 80 | test('should return an array', () => { 81 | expect(Array.isArray(results)).toBe(true); 82 | }); 83 | test('should parse the values with the given logic in the function', () => { 84 | expect(results).toEqual(['T', 'e', 's', 't']); 85 | }); 86 | }); 87 | 88 | describe('utils.pad', () => { 89 | test('should return a string', () => { 90 | expect(typeof pad(12, 4)).toBe('string'); 91 | }); 92 | test('should return a string with the give length', () => { 93 | const len = 12; 94 | expect(pad(12, len).length).toBe(len); 95 | }); 96 | test('should return a string respresentation of a number with leading zeros', () => { 97 | expect(pad(12, 4)).toBe('0012'); 98 | }); 99 | test('should return a string respresentation of a hexstring with leading zeros', () => { 100 | expect(pad('11', 4)).toBe('0011'); 101 | }); 102 | }); 103 | 104 | describe('utils.handleError', () => { 105 | test('Should write Error Message to console', () => { 106 | const throwErrorFunction = (): void => { 107 | throw new Error('Fatal Error'); 108 | }; 109 | const logSpy = jest.spyOn(global.console, 'log'); 110 | 111 | try { 112 | throwErrorFunction(); 113 | } catch (err) { 114 | const e = err as Error; 115 | handleError(e); 116 | } 117 | 118 | expect(logSpy).toHaveBeenCalled(); 119 | expect(logSpy).toHaveBeenCalledTimes(1); 120 | expect(logSpy).toHaveBeenCalledWith(new Error('Fatal Error')); 121 | expect(logSpy.mock.calls).toContainEqual([new Error('Fatal Error')]); 122 | 123 | logSpy.mockRestore(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /cert_url.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://railpublickey.uic.org/download.php", 3 | "fileName": "keys.json" 4 | } 5 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | import type { Config } from 'jest'; 7 | 8 | const config: Config = { 9 | // All imported modules in your tests should be mocked automatically 10 | // automock: false, 11 | 12 | // Stop running tests after `n` failures 13 | // bail: 0, 14 | 15 | // The directory where Jest should store its cached dependency information 16 | // cacheDirectory: "/private/var/folders/xq/4_kkcsnj6p33q76__55d_81w0000gp/T/jest_dy", 17 | 18 | // Automatically clear mock calls, instances, contexts and results before every test 19 | clearMocks: true, 20 | 21 | // Indicates whether the coverage information should be collected while executing the test 22 | collectCoverage: true, 23 | 24 | // An array of glob patterns indicating a set of files for which coverage information should be collected 25 | collectCoverageFrom: ['src/**/*.ts', 'src/**/*.mts', '!src/**/*.d.ts', '!src/**/*.d.mts'], 26 | 27 | // The directory where Jest should output its coverage files 28 | coverageDirectory: 'coverage', 29 | 30 | // An array of regexp pattern strings used to skip coverage collection 31 | coveragePathIgnorePatterns: ['/node_modules/', '/src/FieldsType.ts'], 32 | 33 | // Indicates which provider should be used to instrument code for coverage 34 | coverageProvider: 'v8', 35 | 36 | // A list of reporter names that Jest uses when writing coverage reports 37 | // coverageReporters: [ 38 | // "json", 39 | // "text", 40 | // "lcov", 41 | // "clover" 42 | // ], 43 | 44 | // An object that configures minimum threshold enforcement for coverage results 45 | // coverageThreshold: undefined, 46 | 47 | // A path to a custom dependency extractor 48 | // dependencyExtractor: undefined, 49 | 50 | // Make calling deprecated APIs throw helpful error messages 51 | // errorOnDeprecated: false, 52 | 53 | // The default configuration for fake timers 54 | // fakeTimers: { 55 | // "enableGlobally": false 56 | // }, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "mjs", 82 | // "cjs", 83 | // "jsx", 84 | // "ts", 85 | // "tsx", 86 | // "json", 87 | // "node" 88 | // ], 89 | 90 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 91 | // moduleNameMapper: {}, 92 | 93 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 94 | // modulePathIgnorePatterns: [], 95 | 96 | // Activates notifications for test results 97 | // notify: false, 98 | 99 | // An enum that specifies notification mode. Requires { notify: true } 100 | // notifyMode: "failure-change", 101 | 102 | // A preset that is used as a base for Jest's configuration 103 | // preset: undefined, 104 | 105 | // Run tests from one or more projects 106 | // projects: undefined, 107 | 108 | // Use this configuration option to add custom reporters to Jest 109 | // reporters: undefined, 110 | 111 | // Automatically reset mock state before every test 112 | // resetMocks: false, 113 | 114 | // Reset the module registry before running each individual test 115 | // resetModules: false, 116 | 117 | // A path to a custom resolver 118 | // resolver: undefined, 119 | 120 | // Automatically restore mock state and implementation before every test 121 | // restoreMocks: false, 122 | 123 | // The root directory that Jest should scan for tests and modules within 124 | // rootDir: undefined, 125 | 126 | // A list of paths to directories that Jest should use to search for files in 127 | // roots: [ 128 | // "" 129 | // ], 130 | 131 | // Allows you to use a custom runner instead of Jest's default test runner 132 | // runner: "jest-runner", 133 | 134 | // The paths to modules that run some code to configure or set up the testing environment before each test 135 | // setupFiles: [], 136 | 137 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 138 | // setupFilesAfterEnv: [], 139 | 140 | // The number of seconds after which a test is considered as slow and reported as such in the results. 141 | // slowTestThreshold: 5, 142 | 143 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 144 | // snapshotSerializers: [], 145 | 146 | // The test environment that will be used for testing 147 | // testEnvironment: "jest-environment-node", 148 | 149 | // Options that will be passed to the testEnvironment 150 | // testEnvironmentOptions: {}, 151 | 152 | // Adds a location field to test results 153 | // testLocationInResults: false, 154 | 155 | // The glob patterns Jest uses to detect test files 156 | // testMatch: [ 157 | // "**/__tests__/**/*.[jt]s?(x)", 158 | // "**/?(*.)+(spec|test).[tj]s?(x)" 159 | // ], 160 | 161 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 162 | testPathIgnorePatterns: ['/node_modules/', '/__tests__/helper.ts'], 163 | 164 | // The regexp pattern or array of patterns that Jest uses to detect test files 165 | // testRegex: [], 166 | 167 | // This option allows the use of a custom results processor 168 | // testResultsProcessor: undefined, 169 | 170 | // This option allows use of a custom test runner 171 | // testRunner: "jest-circus/runner", 172 | 173 | // A map from regular expressions to paths to transformers 174 | // transform: undefined, 175 | 176 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 177 | // transformIgnorePatterns: [ 178 | // "/node_modules/", 179 | // "\\.pnp\\.[^\\/]+$" 180 | // ], 181 | 182 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 183 | // unmockedModulePathPatterns: undefined, 184 | 185 | // Indicates whether each individual test should be reported during the run 186 | // verbose: undefined, 187 | 188 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 189 | // watchPathIgnorePatterns: [], 190 | 191 | // Whether to use watchman for file crawling 192 | // watchman: true, 193 | 194 | preset: 'ts-jest', 195 | testEnvironment: 'node' 196 | }; 197 | 198 | export default config; 199 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uic-918-3", 3 | "version": "1.0.5", 4 | "description": "Package for decoding and parsing barcodes according to UIC-918.3 specification, which are used commonly on public transport online tickets.", 5 | "main": "build/src/index.js", 6 | "types": "build/src/index.d.ts", 7 | "scripts": { 8 | "test": "jest", 9 | "test:watch": "jest --watch", 10 | "clean": "rimraf coverage build tmp postinstall", 11 | "postinstall": "node postinstall/updateCerts.js", 12 | "build": "tsc -p tsconfig.json", 13 | "build:postinstall": "tsc -p tsconfig.postinstall.json", 14 | "build:watch": "tsc -w -p tsconfig.json", 15 | "build:release": "npm run lint && npm run clean && tsc -p tsconfig.release.json && npm run build:postinstall", 16 | "lint": "eslint . --ext .ts --ext .mts", 17 | "prettier": "prettier --config .prettierrc --write .", 18 | "prepare": "husky" 19 | }, 20 | "author": "Francis Doege", 21 | "contributors": [ 22 | { 23 | "name": "Arne Breitsprecher", 24 | "url": "https://github.com/arnebr" 25 | }, 26 | { 27 | "name": "Ulf Winkelvos", 28 | "url": "https://github.com/uwinkelvos" 29 | } 30 | ], 31 | "license": "MIT", 32 | "dependencies": { 33 | "axios": "^1.6.3", 34 | "jsrsasign": "^11.0.0", 35 | "lodash": "^4.17.11", 36 | "tslib": "^2.6.2", 37 | "xml2js": "^0.6.2", 38 | "zxing-wasm": "^1.2.3" 39 | }, 40 | "devDependencies": { 41 | "@jest/globals": "^29.7.0", 42 | "@types/jsrsasign": "^10.5.12", 43 | "@types/lodash": "^4.14.202", 44 | "@types/node": "^20.10.8", 45 | "@types/randomstring": "^1.1.11", 46 | "@types/xml2js": "^0.4.14", 47 | "@typescript-eslint/eslint-plugin": "^7.0.2", 48 | "@typescript-eslint/parser": "^7.0.2", 49 | "eslint": "^8.56.0", 50 | "eslint-config-prettier": "^9.1.0", 51 | "eslint-plugin-jest": "^27.9.0", 52 | "eslint-plugin-prettier": "^5.1.3", 53 | "husky": "^9.0.11", 54 | "jest": "^29.7.0", 55 | "prettier": "^3.2.5", 56 | "randomstring": "^1.1.5", 57 | "rimraf": "^5.0.5", 58 | "ts-jest": "^29.1.2", 59 | "ts-node": "^10.9.2", 60 | "typescript": "^5.3.3" 61 | }, 62 | "repository": { 63 | "type": "git", 64 | "url": "git+https://github.com/justusjonas74/uic-918-3.git" 65 | }, 66 | "keywords": [ 67 | "uic-918-3", 68 | "online-ticket", 69 | "deutsche-bahn", 70 | "barcode", 71 | "aztec" 72 | ], 73 | "bugs": { 74 | "url": "https://github.com/justusjonas74/uic-918-3/issues" 75 | }, 76 | "homepage": "https://github.com/justusjonas74/uic-918-3#readme" 77 | } 78 | -------------------------------------------------------------------------------- /postinstall/updateCerts.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /postinstall/updateCerts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | const updateLocalCerts_1 = require('./updateLocalCerts'); 4 | (0, updateLocalCerts_1.updateLocalCerts)(); 5 | -------------------------------------------------------------------------------- /postinstall/updateLocalCerts.d.ts: -------------------------------------------------------------------------------- 1 | export declare const filePath: string; 2 | export declare const updateLocalCerts: () => Promise; 3 | -------------------------------------------------------------------------------- /postinstall/updateLocalCerts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | Object.defineProperty(exports, '__esModule', { value: true }); 3 | exports.updateLocalCerts = exports.filePath = void 0; 4 | const tslib_1 = require('tslib'); 5 | const path_1 = require('path'); 6 | const fs_1 = require('fs'); 7 | const axios_1 = tslib_1.__importDefault(require('axios')); 8 | const xml2js = tslib_1.__importStar(require('xml2js')); 9 | const parser = new xml2js.Parser(); 10 | const { url, fileName } = JSON.parse((0, fs_1.readFileSync)('./cert_url.json', 'utf8')); 11 | const basePath = (0, path_1.dirname)('./cert_url.json'); 12 | exports.filePath = (0, path_1.join)(basePath, fileName); 13 | const updateLocalCerts = () => 14 | tslib_1.__awaiter(void 0, void 0, void 0, function* () { 15 | try { 16 | console.log(`Load public keys from ${url} ...`); 17 | const response = yield axios_1.default.get(url); 18 | if (response && response.status == 200) { 19 | console.log(`Successfully loaded key file.`); 20 | } 21 | parser.parseString(response.data, function (err, result) { 22 | if (!err) { 23 | (0, fs_1.writeFileSync)(exports.filePath, JSON.stringify(result)); 24 | console.log(`Loaded ${result.keys.key.length} public keys and saved under "${exports.filePath}".`); 25 | } else { 26 | console.log(err); 27 | } 28 | }); 29 | } catch (error) { 30 | console.log(error); 31 | } 32 | }); 33 | exports.updateLocalCerts = updateLocalCerts; 34 | -------------------------------------------------------------------------------- /src/FieldsType.ts: -------------------------------------------------------------------------------- 1 | import { TicketDataContainer } from './barcode-data'; 2 | import { DC_LISTE_TYPE, RCT2_BLOCK } from './block-types'; 3 | import { interpretFieldResult } from './utils'; 4 | 5 | export type SupportedTypes = 6 | | Date 7 | | string 8 | | number 9 | | Buffer 10 | | DC_LISTE_TYPE 11 | | RCT2_BLOCK[] 12 | | interpretFieldResult 13 | | TicketDataContainer; 14 | 15 | export type InterpreterFunctionType = (x: Buffer) => T; 16 | export type InterpreterArrayFunctionType = (x: Buffer) => T[]; 17 | 18 | export interface FieldsType { 19 | length?: number; 20 | name: string; 21 | interpreterFn?: InterpreterFunctionType; 22 | } 23 | -------------------------------------------------------------------------------- /src/TicketContainer.ts: -------------------------------------------------------------------------------- 1 | import { FieldsType } from './FieldsType'; 2 | 3 | import TC_U_HEAD_01 from './ticketDataContainers/TC_U_HEAD_01'; 4 | import TC_0080VU_01 from './ticketDataContainers/TC_0080VU_01'; 5 | import TC_1180AI_01 from './ticketDataContainers/TC_1180AI_01'; 6 | import TC_0080BL_02 from './ticketDataContainers/TC_0080BL_02'; 7 | import TC_0080BL_03 from './ticketDataContainers/TC_0080BL_03'; 8 | import TC_0080ID_01 from './ticketDataContainers/TC_0080ID_01'; 9 | import TC_0080ID_02 from './ticketDataContainers/TC_0080ID_02'; 10 | import TC_U_TLAY_01 from './ticketDataContainers/TC_U_TLAY_01'; 11 | 12 | type TicketContainerTypeVersions = '01' | '02' | '03'; 13 | 14 | export interface TicketContainerType { 15 | name: string; 16 | version: TicketContainerTypeVersions; 17 | dataFields: FieldsType[]; 18 | } 19 | 20 | export const TicketContainer: TicketContainerType[] = [ 21 | TC_U_HEAD_01, 22 | TC_0080VU_01, 23 | TC_1180AI_01, 24 | TC_0080BL_02, 25 | TC_0080BL_03, 26 | TC_0080ID_01, 27 | TC_0080ID_02, 28 | TC_U_TLAY_01 29 | ]; 30 | 31 | export default TicketContainer; 32 | -------------------------------------------------------------------------------- /src/barcode-data.ts: -------------------------------------------------------------------------------- 1 | import { unzipSync } from 'zlib'; 2 | 3 | import TicketContainer, { TicketContainerType } from './TicketContainer'; 4 | import { interpretField, interpretFieldResult, parseContainers, parsingFunction } from './utils'; 5 | import { SupportedTypes } from './FieldsType'; 6 | import { verifyTicket, TicketSignatureVerficationStatus } from './check_signature'; 7 | 8 | // Get raw data and uncompress the TicketData 9 | function getVersion(data: Buffer): number { 10 | return parseInt(data.subarray(3, 5).toString(), 10); 11 | } 12 | 13 | function getLengthOfSignatureByVersion(version: number): number { 14 | if (version !== 1 && version !== 2) { 15 | throw new Error( 16 | `Barcode header contains a version of ${version} (instead of 1 or 2), which is not supported by this library yet.` 17 | ); 18 | } 19 | const lengthOfSignature = version === 1 ? 50 : 64; 20 | return lengthOfSignature; 21 | } 22 | 23 | export type BarcodeHeader = { 24 | umid: Buffer; 25 | mt_version: Buffer; 26 | rics: Buffer; 27 | key_id: Buffer; 28 | }; 29 | 30 | function getHeader(data: Buffer): BarcodeHeader { 31 | const umid = data.subarray(0, 3); 32 | const mt_version = data.subarray(3, 5); 33 | const rics = data.subarray(5, 9); 34 | const key_id = data.subarray(9, 14); 35 | return { umid, mt_version, rics, key_id }; 36 | } 37 | 38 | function getSignature(data: Buffer, version: number): Buffer { 39 | return data.subarray(14, 14 + getLengthOfSignatureByVersion(version)); 40 | } 41 | 42 | function getTicketDataLength(data: Buffer, version: number): Buffer { 43 | return data.subarray(getLengthOfSignatureByVersion(version) + 14, getLengthOfSignatureByVersion(version) + 18); 44 | } 45 | 46 | function getTicketDataRaw(data: Buffer, version: number): Buffer { 47 | return data.subarray(getLengthOfSignatureByVersion(version) + 18, data.length); 48 | } 49 | 50 | function getTicketDataUncompressed(data: Buffer): Buffer { 51 | if (data && data.length > 0) { 52 | return unzipSync(data); 53 | } else { 54 | return data; 55 | } 56 | } 57 | 58 | // Interpreters for uncompressed Ticket Data 59 | export class TicketDataContainer { 60 | id: string; 61 | version: string; 62 | length: number; 63 | container_data: Buffer | interpretFieldResult; 64 | constructor(data: Buffer) { 65 | this.id = data.subarray(0, 6).toString(); 66 | this.version = data.subarray(6, 8).toString(); 67 | this.length = parseInt(data.subarray(8, 12).toString(), 10); 68 | this.container_data = TicketDataContainer.parseFields(this.id, this.version, data.subarray(12, data.length)); 69 | } 70 | 71 | static parseFields(id: string, version: string, data: Buffer): Buffer | interpretFieldResult { 72 | const fields = getBlockTypeFieldsByIdAndVersion(id, version); 73 | if (fields) { 74 | return interpretField(data, fields.dataFields); 75 | } else { 76 | console.log(`ALERT: Container with id ${id} and version ${version} isn't implemented for TicketContainer ${id}.`); 77 | return data; 78 | } 79 | } 80 | } 81 | 82 | const interpretTicketContainer: parsingFunction = (data: Buffer): [TicketDataContainer, Buffer] => { 83 | const length = parseInt(data.subarray(8, 12).toString(), 10); 84 | const remainder = data.subarray(length, data.length); 85 | const container = new TicketDataContainer(data.subarray(0, length)); 86 | return [container, remainder]; 87 | }; 88 | 89 | function getBlockTypeFieldsByIdAndVersion(id: string, version: string): TicketContainerType | undefined { 90 | return TicketContainer.find((ticketContainer) => ticketContainer.name === id && ticketContainer.version === version); 91 | } 92 | export type ParsedUIC918Barcode = { 93 | version: number; 94 | header: BarcodeHeader; 95 | signature: Buffer; 96 | ticketDataLength: Buffer; 97 | ticketDataRaw: Buffer; 98 | ticketDataUncompressed: Buffer; 99 | ticketContainers: SupportedTypes[]; 100 | validityOfSignature?: TicketSignatureVerficationStatus; 101 | isSignatureValid?: boolean; 102 | }; 103 | async function parseBarcodeData(data: Buffer, verifySignature: boolean = false): Promise { 104 | const version = getVersion(data); 105 | const ticketDataRaw = getTicketDataRaw(data, version); 106 | const ticketDataUncompressed = getTicketDataUncompressed(ticketDataRaw); 107 | const ticketContainers = parseContainers(ticketDataUncompressed, interpretTicketContainer); 108 | const ticket: ParsedUIC918Barcode = { 109 | version, 110 | header: getHeader(data), 111 | signature: getSignature(data, version), 112 | ticketDataLength: getTicketDataLength(data, version), 113 | ticketDataRaw, 114 | ticketDataUncompressed, 115 | ticketContainers 116 | }; 117 | if (verifySignature) { 118 | const validityOfSignature = await verifyTicket(ticket); 119 | ticket.validityOfSignature = validityOfSignature; 120 | if (validityOfSignature === TicketSignatureVerficationStatus.VALID) { 121 | ticket.isSignatureValid = true; 122 | } else if (validityOfSignature === TicketSignatureVerficationStatus.INVALID) { 123 | ticket.isSignatureValid = false; 124 | } 125 | } 126 | return ticket; 127 | } 128 | 129 | export default parseBarcodeData; 130 | -------------------------------------------------------------------------------- /src/barcode-reader.ts: -------------------------------------------------------------------------------- 1 | import { ReaderOptions, readBarcodesFromImageFile } from 'zxing-wasm'; 2 | const defaultOptions: ReaderOptions = { 3 | tryHarder: true, 4 | formats: ['Aztec'] 5 | }; 6 | 7 | export async function ZXing(data: string | Buffer, options: ReaderOptions = defaultOptions): Promise { 8 | const [barcodeResult] = await readBarcodesFromImageFile(new Blob([data]), options); 9 | if (!barcodeResult || !barcodeResult.isValid || !barcodeResult.bytes) { 10 | throw new Error('Could not detect a valid Aztec barcode'); 11 | } 12 | return Buffer.from(barcodeResult.bytes); 13 | } 14 | -------------------------------------------------------------------------------- /src/block-types.ts: -------------------------------------------------------------------------------- 1 | import { id_types, sBlockTypes, orgid, efm_produkt, tarifpunkt, EFM_Produkt } from './enums'; 2 | 3 | import { FieldsType, InterpreterFunctionType } from './FieldsType'; 4 | import { 5 | interpretField, 6 | interpretFieldResult as InterpretFieldResult, 7 | pad, 8 | parseContainers, 9 | parsingFunction 10 | } from './utils'; 11 | 12 | // ################ 13 | // DATA TYPES 14 | // ################ 15 | 16 | export const STRING: InterpreterFunctionType = (x: Buffer) => x.toString(); 17 | export const HEX: InterpreterFunctionType = (x: Buffer) => x.toString('hex'); 18 | export const STR_INT: InterpreterFunctionType = (x: Buffer) => parseInt(x.toString(), 10); 19 | export const INT: InterpreterFunctionType = (x: Buffer) => x.readUIntBE(0, x.length); 20 | export const DB_DATETIME: InterpreterFunctionType = (x: Buffer) => { 21 | // DDMMYYYYHHMM 22 | const day = STR_INT(x.subarray(0, 2)); 23 | const month = STR_INT(x.subarray(2, 4)) - 1; 24 | const year = STR_INT(x.subarray(4, 8)); 25 | const hour = STR_INT(x.subarray(8, 10)); 26 | const minute = STR_INT(x.subarray(10, 12)); 27 | return new Date(year, month, day, hour, minute); 28 | }; 29 | const KA_DATETIME: InterpreterFunctionType = (x: Buffer) => { 30 | // ‘yyyyyyymmmmddddd’B + hhhhhmmmmmmsssss’B (4 Byte) 31 | const dateStr = pad(parseInt(x.toString('hex'), 16).toString(2), 32); 32 | const year = parseInt(dateStr.slice(0, 7), 2) + 1990; 33 | const month = parseInt(dateStr.slice(7, 11), 2) - 1; 34 | const day = parseInt(dateStr.slice(11, 16), 2); 35 | const hour = parseInt(dateStr.slice(16, 21), 2); 36 | const minute = parseInt(dateStr.slice(21, 27), 2); 37 | const sec = parseInt(dateStr.slice(27, 32), 2) / 2; 38 | return new Date(year, month, day, hour, minute, sec); 39 | }; 40 | 41 | const ORG_ID = (x: Buffer): string => { 42 | const id = INT(x); 43 | return orgid(id); 44 | }; 45 | 46 | const EFM_PRODUKT = (x: Buffer): EFM_Produkt => { 47 | const orgId = INT(x.subarray(2, 4)); 48 | const produktNr = INT(x.subarray(0, 2)); 49 | return efm_produkt(orgId, produktNr); 50 | }; 51 | export const AUSWEIS_TYP = (x: Buffer): string => { 52 | const number = STR_INT(x); 53 | return id_types[number]; 54 | }; 55 | 56 | export interface DC_LISTE_TYPE { 57 | tagName: string; 58 | dc_length: number; 59 | typ_DC: string; 60 | pv_org_id: number; 61 | TP: string[]; 62 | } 63 | 64 | const DC_LISTE = (x: Buffer): DC_LISTE_TYPE => { 65 | const tagName = HEX(x.subarray(0, 1)); 66 | const dc_length = INT(x.subarray(1, 2)); 67 | const typ_DC = HEX(x.subarray(2, 3)); 68 | const pv_org_id = INT(x.subarray(3, 5)); 69 | const TP_RAW = splitDCList(dc_length, typ_DC, x.subarray(5, x.length)); 70 | const TP = TP_RAW.map((item) => tarifpunkt(pv_org_id, item)); 71 | return { tagName, dc_length, typ_DC, pv_org_id, TP }; 72 | }; 73 | 74 | const EFS_FIELDS: FieldsType[] = [ 75 | { 76 | name: 'berechtigungs_nr', 77 | length: 4, 78 | interpreterFn: INT 79 | }, 80 | { 81 | name: 'kvp_organisations_id', 82 | length: 2, 83 | interpreterFn: ORG_ID 84 | }, 85 | { 86 | name: 'efm_produkt', 87 | length: 4, 88 | interpreterFn: EFM_PRODUKT 89 | }, 90 | { 91 | name: 'valid_from', 92 | length: 4, 93 | interpreterFn: KA_DATETIME 94 | }, 95 | { 96 | name: 'valid_to', 97 | length: 4, 98 | interpreterFn: KA_DATETIME 99 | }, 100 | { 101 | name: 'preis', 102 | length: 3, 103 | interpreterFn: INT 104 | }, 105 | { 106 | name: 'sam_seqno', 107 | length: 4, 108 | interpreterFn: INT 109 | }, 110 | { 111 | name: 'lengthList_DC', 112 | length: 1, 113 | interpreterFn: INT 114 | }, 115 | { 116 | name: 'Liste_DC', 117 | length: undefined, 118 | interpreterFn: DC_LISTE 119 | } 120 | ]; 121 | 122 | export type IEFS_DATA = Record; 123 | export const EFS_DATA = (x: Buffer): IEFS_DATA => { 124 | const lengthListDC = INT(x.subarray(25, 26)); 125 | 126 | const t = [x.subarray(0, lengthListDC + 26)]; 127 | 128 | if (lengthListDC + 26 < x.length) { 129 | t.push(x.subarray(lengthListDC + 26, x.length)); 130 | } 131 | const res: IEFS_DATA = {}; 132 | t.forEach((ticket, index) => { 133 | res[1 + index] = interpretField(ticket, EFS_FIELDS); 134 | }); 135 | return res; 136 | }; 137 | 138 | function splitDCList(dcLength: number, typDC: string, data: Buffer): number[] { 139 | // 0x0D 3 Byte CT, CM 140 | // 0x10 2 Byte Länder,SWT, QDL 141 | let SEP: number; 142 | if (parseInt(typDC, 16) === 0x10) { 143 | SEP = 2; 144 | } else { 145 | SEP = 3; 146 | } 147 | const amount = (dcLength - 3) / SEP; 148 | const res = []; 149 | for (let i = 0; i < amount; i++) { 150 | res.push(INT(data.subarray(i * SEP, i * SEP + SEP))); 151 | } 152 | return res; 153 | } 154 | 155 | export type RCT2_BLOCK = { 156 | line: number; 157 | column: number; 158 | height: number; 159 | width: number; 160 | style: number; 161 | value: string; 162 | }; 163 | 164 | const interpretRCT2Block: parsingFunction = (data: Buffer): [RCT2_BLOCK, Buffer] => { 165 | const line = parseInt(data.subarray(0, 2).toString(), 10); 166 | const column = parseInt(data.subarray(2, 4).toString(), 10); 167 | const height = parseInt(data.subarray(4, 6).toString(), 10); 168 | const width = parseInt(data.subarray(6, 8).toString(), 10); 169 | const style = parseInt(data.subarray(8, 9).toString(), 10); 170 | const length = parseInt(data.subarray(9, 13).toString(), 10); 171 | const value = data.subarray(13, 13 + length).toString(); 172 | const res: RCT2_BLOCK = { 173 | line, 174 | column, 175 | height, 176 | width, 177 | style, 178 | value 179 | }; 180 | const rem = data.subarray(13 + length); 181 | return [res, rem]; 182 | }; 183 | 184 | export const RCT2_BLOCKS = (x: Buffer): RCT2_BLOCK[] => { 185 | return parseContainers(x, interpretRCT2Block) as RCT2_BLOCK[]; 186 | }; 187 | 188 | const A_BLOCK_FIELDS_V2: FieldsType[] = [ 189 | { 190 | name: 'certificate', 191 | length: 11, 192 | interpreterFn: STRING 193 | }, 194 | { 195 | name: 'padding', 196 | length: 11, 197 | interpreterFn: HEX 198 | }, 199 | { 200 | name: 'valid_from', 201 | length: 8, 202 | interpreterFn: STRING 203 | }, 204 | { 205 | name: 'valid_to', 206 | length: 8, 207 | interpreterFn: STRING 208 | }, 209 | { 210 | name: 'serial', 211 | length: 8, 212 | interpreterFn: STRING 213 | } 214 | ]; 215 | 216 | const A_BLOCK_FIELDS_V3: FieldsType[] = [ 217 | { 218 | name: 'valid_from', 219 | length: 8, 220 | interpreterFn: STRING 221 | }, 222 | { 223 | name: 'valid_to', 224 | length: 8, 225 | interpreterFn: STRING 226 | }, 227 | { 228 | name: 'serial', 229 | length: 10, 230 | interpreterFn: STRING 231 | } 232 | ]; 233 | 234 | const interpretSingleSBlock: parsingFunction = (data: Buffer): [Record, Buffer] => { 235 | const res: Record = {}; 236 | const type = sBlockTypes[parseInt(data.subarray(1, 4).toString(), 10)]; 237 | const length = parseInt(data.subarray(4, 8).toString(), 10); 238 | res[type] = data.subarray(8, 8 + length).toString(); 239 | const rem = data.subarray(8 + length); 240 | return [res, rem]; 241 | }; 242 | 243 | export const auftraegeSBlocksV2 = (x: Buffer): InterpretFieldResult => { 244 | const A_LENGTH = 11 + 11 + 8 + 8 + 8; 245 | return auftraegeSblocks(x, A_LENGTH, A_BLOCK_FIELDS_V2); 246 | }; 247 | 248 | export const auftraegeSBlocksV3 = (x: Buffer): InterpretFieldResult => { 249 | const A_LENGTH = 10 + 8 + 8; 250 | return auftraegeSblocks(x, A_LENGTH, A_BLOCK_FIELDS_V3); 251 | }; 252 | 253 | function auftraegeSblocks(x: Buffer, A_LENGTH: number, fields: FieldsType[]): InterpretFieldResult { 254 | const res: InterpretFieldResult = {}; 255 | res.auftrag_count = parseInt(x.subarray(0, 1).toString(), 10); 256 | for (let i = 0; i < res.auftrag_count; i++) { 257 | const bez = `auftrag_${i + 1}`; 258 | res[bez] = interpretField(x.subarray(1 + i * A_LENGTH, (i + 1) * A_LENGTH + 1), fields); 259 | } 260 | res.sblock_amount = parseInt( 261 | x.subarray(A_LENGTH * res.auftrag_count + 1, A_LENGTH * res.auftrag_count + 3).toString(), 262 | 10 263 | ); 264 | const sblock_containers = parseContainers(x.subarray(A_LENGTH * res.auftrag_count + 3), interpretSingleSBlock); 265 | res.sblocks = Object.assign({}, ...sblock_containers); 266 | return res; 267 | } 268 | -------------------------------------------------------------------------------- /src/checkInput.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, promises } from 'fs'; 2 | import { isAbsolute, join } from 'path'; 3 | 4 | const { readFile } = promises; 5 | 6 | const tryToLoadFile = async (filePath: string): Promise => { 7 | const fullPath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath); 8 | return await readFile(fullPath); 9 | }; 10 | 11 | export const loadFileOrBuffer = async (input: PathLike | Buffer): Promise => { 12 | const inputIsString = typeof input === 'string'; 13 | const inputIsBuffer = input instanceof Buffer; 14 | 15 | if (!inputIsBuffer && !inputIsString) { 16 | throw new Error('Error: Input must be a Buffer (Image) or a String (path to image)'); 17 | } 18 | 19 | if (inputIsString) { 20 | return tryToLoadFile(input); 21 | } 22 | 23 | return input; 24 | }; 25 | -------------------------------------------------------------------------------- /src/check_signature.ts: -------------------------------------------------------------------------------- 1 | import * as rs from 'jsrsasign'; 2 | 3 | import { Key, getCertByID } from './get_certs'; 4 | import { BarcodeHeader, ParsedUIC918Barcode } from './barcode-data'; 5 | 6 | function checkSignature( 7 | certPEM: rs.RSAKey | rs.KJUR.crypto.DSA | rs.KJUR.crypto.ECDSA, 8 | signature: string, 9 | message: string 10 | ): boolean { 11 | // DSA signature validation 12 | const sig = new rs.KJUR.crypto.Signature({ alg: 'SHA1withDSA' }); 13 | sig.init(certPEM); 14 | sig.updateHex(message); 15 | return sig.verify(signature); 16 | } 17 | 18 | async function getCertByHeader(header: BarcodeHeader): Promise { 19 | const orgId = parseInt(header.rics.toString(), 10); 20 | const keyId = parseInt(header.key_id.toString(), 10); 21 | const cert = await getCertByID(orgId, keyId); 22 | return cert; 23 | } 24 | 25 | export enum TicketSignatureVerficationStatus { 26 | VALID = 'VALID', 27 | INVALID = 'INVALID', 28 | NOPUBLICKEY = 'Public Key not found' 29 | } 30 | 31 | export const verifyTicket = async function (ticket: ParsedUIC918Barcode): Promise { 32 | const cert = await getCertByHeader(ticket.header); 33 | if (!cert) { 34 | console.log("No certificate found. Signature couldn't been proofed."); 35 | return TicketSignatureVerficationStatus.NOPUBLICKEY; 36 | } 37 | 38 | const modifiedCert = '-----BEGIN CERTIFICATE-----\n' + cert.publicKey + '\n-----END CERTIFICATE-----\n'; 39 | const publicKey = rs.KEYUTIL.getKey(modifiedCert); 40 | const isSignatureValid = checkSignature( 41 | publicKey, 42 | ticket.signature.toString('hex'), 43 | ticket.ticketDataRaw.toString('hex') 44 | ); 45 | 46 | return isSignatureValid ? TicketSignatureVerficationStatus.VALID : TicketSignatureVerficationStatus.INVALID; 47 | }; 48 | -------------------------------------------------------------------------------- /src/enums.ts: -------------------------------------------------------------------------------- 1 | import KA_DATA from './ka-data'; 2 | 3 | export const orgid = (orgId: number): string => { 4 | return KA_DATA.org_id[orgId] || orgId.toString(); 5 | }; 6 | 7 | export const tarifpunkt = (orgId: number, tp: number): string => { 8 | if (KA_DATA.tarifpunkte[orgId] && KA_DATA.tarifpunkte[orgId][tp]) { 9 | return KA_DATA.tarifpunkte[orgId][tp]; 10 | } else { 11 | return tp.toString(); 12 | } 13 | }; 14 | 15 | export type EFM_Produkt = { 16 | kvp_organisations_id: string; 17 | produkt_nr: string; 18 | }; 19 | 20 | export const efm_produkt = (orgId: number, produktId: number): EFM_Produkt => { 21 | const kvp_organisations_id = orgid(orgId); 22 | const produkt_nr = 23 | KA_DATA.efmprodukte[orgId] && KA_DATA.efmprodukte[orgId][produktId] 24 | ? KA_DATA.efmprodukte[orgId][produktId] 25 | : produktId.toString(); 26 | return { kvp_organisations_id, produkt_nr }; 27 | }; 28 | 29 | export enum sBlockTypes { 30 | 'Preismodell' = 1, 31 | 'Produktklasse Gesamtticket' = 2, 32 | 'Produktklasse Hinfahrt' = 3, 33 | 'Produktklasse Rückfahrt' = 4, 34 | 'Passagiere' = 9, 35 | 'Kinder' = 12, 36 | 'Klasse' = 14, 37 | 'H-Start-Bf' = 15, 38 | 'H-Ziel-Bf' = 16, 39 | 'R-Start-Bf' = 17, 40 | 'R-Ziel-Bf' = 18, 41 | 'Vorgangsnr./Flugscheinnr.' = 19, 42 | 'Vertragspartner' = 20, 43 | 'VIA' = 21, 44 | 'Personenname' = 23, 45 | 'Preisart' = 26, 46 | 'Ausweis-ID' = 27, 47 | 'Vorname, Name' = 28, 48 | 'Gueltig von' = 31, 49 | 'Gueltig bis' = 32, 50 | 'Start-Bf-ID' = 35, 51 | 'Ziel-Bf-ID' = 36, 52 | 'Anzahl Personen' = 40, 53 | 'TBD EFS Anzahl' = 41 54 | } 55 | 56 | export enum id_types { 57 | 'CC' = 1, 58 | 'BC' = 4, 59 | 'EC' = 7, 60 | 'Bonus.card business' = 8, 61 | 'Personalausweis' = 9, 62 | 'Reisepass' = 10, 63 | 'bahn.bonus Card' = 11 64 | } 65 | -------------------------------------------------------------------------------- /src/get_certs.ts: -------------------------------------------------------------------------------- 1 | import { PathLike, promises } from 'fs'; 2 | import { dirname, join } from 'path'; 3 | 4 | const { readFile } = promises; 5 | 6 | import { find } from 'lodash'; 7 | 8 | import { fileName } from '../cert_url.json'; 9 | const basePath = dirname(require.resolve('../cert_url.json')); 10 | const filePath = join(basePath, fileName); 11 | 12 | export interface UICKeys { 13 | keys: Keys; 14 | } 15 | 16 | export interface Keys { 17 | key: Key[]; 18 | } 19 | 20 | export interface Key { 21 | issuerName: string[]; 22 | issuerCode: string[]; 23 | versionType: string[]; 24 | signatureAlgorithm: string[]; 25 | id: string[]; 26 | publicKey: string[]; 27 | barcodeVersion: string[]; 28 | startDate: Date[]; 29 | endDate: Date[]; 30 | barcodeXsd: BarcodeXSD[]; 31 | allowedProductOwnerCodes: Array; 32 | keyForged: string[]; 33 | commentForEncryptionType: string[]; 34 | } 35 | 36 | export interface AllowedProductOwnerCodeClass { 37 | productOwnerCode: string[]; 38 | productOwnerName: string[]; 39 | } 40 | 41 | export enum BarcodeXSD { 42 | Empty = '', 43 | String = 'String' 44 | } 45 | 46 | const openLocalFiles = async (filePath: PathLike): Promise => { 47 | try { 48 | const file = await readFile(filePath, 'utf8'); 49 | const uicKey = JSON.parse(file); 50 | return uicKey; 51 | } catch (error) { 52 | throw new Error(`Couldn't read file ${filePath}. ` + error); 53 | } 54 | }; 55 | 56 | const selectCert = (keys: UICKeys, ricsCode: number, keyId: number): Key | undefined => { 57 | const searchPattern = { 58 | issuerCode: [ricsCode.toString()], 59 | id: [keyId.toString()] 60 | }; 61 | const cert = find(keys.keys.key, searchPattern); 62 | if (!cert) { 63 | console.log(`Couldn't find a certificate for issuer ${ricsCode} and key number ${keyId}`); 64 | } 65 | return cert; 66 | }; 67 | 68 | export const getCertByID = async (orgId: number, keyId: number, path: string = filePath): Promise => { 69 | try { 70 | const keys = await openLocalFiles(path); 71 | return selectCert(keys, orgId, keyId); 72 | } catch (error) { 73 | console.log(error); 74 | return undefined; 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ZXing } from './barcode-reader'; 2 | import interpretBarcode, { ParsedUIC918Barcode } from './barcode-data'; 3 | import { loadFileOrBuffer } from './checkInput'; 4 | 5 | type ReadBarcodeOptions = { 6 | verifySignature?: boolean; 7 | }; 8 | 9 | export const readBarcode = async function ( 10 | input: string | Buffer, 11 | options?: ReadBarcodeOptions 12 | ): Promise { 13 | const defaults = { 14 | verifySignature: false 15 | }; 16 | const opts: ReadBarcodeOptions = Object.assign({}, defaults, options); 17 | 18 | const imageBuffer = await loadFileOrBuffer(input); 19 | const barcodeData = await ZXing(imageBuffer); 20 | const ticket = await interpretBarcode(barcodeData, opts.verifySignature); 21 | return ticket; 22 | }; 23 | 24 | export { default as interpretBarcode } from './barcode-data'; 25 | -------------------------------------------------------------------------------- /src/ka-data.ts: -------------------------------------------------------------------------------- 1 | // Data from https://assets.static-bahn.de/dam/jcr:78591073-77fd-4e3d-968d-c5a7cf20eec5/mdb_305706_uic_918-3_vdv_stammdaten_version_16_12_2022%20mk.xls 2 | const DB_ORTE: Record = { 3 | 8000001: '8000001 (Aachen Hbf)', 4 | 8000002: '8000002 (Aalen)', 5 | 8000441: '8000441 (Ahlen(Westf))', 6 | 8000605: '8000605 (Arnsberg(Westf))', 7 | 8000010: '8000010 (Aschaffenburg Hbf)', 8 | 8000013: '8000013 (Augsburg Hbf )', 9 | 8000712: '8000712 (Bad Homburg)', 10 | 8000774: '8000774 (Baden-Baden)', 11 | 8000025: '8000025 (Bamberg)', 12 | 8000028: '8000028 (Bayreuth Hbf )', 13 | 8000899: '8000899 (Bergisch-Gladbach)', 14 | 8011155: '8011155 (Berlin Alexanderpl.)', 15 | 8010038: '8010038 (Berlin Friedrichstr )', 16 | 8011102: '8011102 (Berlin Gesundbrunnen)', 17 | 8011160: '8011160 (Berlin Hbf)', 18 | 8010255: '8010255 (Berlin Ostbahnhof)', 19 | 8011118: '8011118 (Berlin Potsdamer Pl)', 20 | 8011113: '8011113 (Berlin Südkreuz)', 21 | 8010405: '8010405 (Berlin Wannsee)', 22 | 8010406: '8010406 (Berlin Zoolg. Garten)', 23 | 8010403: '8010403 (Berlin-Charlottenbg.)', 24 | 8010036: '8010036 (Berlin-Lichtenberg)', 25 | 8010404: '8010404 (Berlin-Spandau)', 26 | 8000036: '8000036 (Bielefeld Hbf )', 27 | 8001055: '8001055 (Böblingen)', 28 | 8000040: '8000040 (Bocholt)', 29 | 8000041: '8000041 (Bochum Hbf)', 30 | 8001038: '8001038 (Bochum-Dahlhausen )', 31 | 8000044: '8000044 (Bonn Hbf)', 32 | 8001083: '8001083 (Bonn-Beuel)', 33 | 8000047: '8000047 (Bottrop Hbf)', 34 | 8000049: '8000049 (Braunschweig Hbf)', 35 | 8000050: '8000050 (Bremen Hbf)', 36 | 8000051: '8000051 (Bremerhaven Hbf)', 37 | 8000054: '8000054 (Brilon Wald)', 38 | 8000059: '8000059 (Bünde(Westf))', 39 | 8000064: '8000064 (Celle)', 40 | 8010184: '8010184 (Chemnitz Hbf)', 41 | 8010073: '8010073 (Cottbus)', 42 | 8000067: '8000067 (Crailsheim)', 43 | 8000068: '8000068 (Darmstadt Hbf)', 44 | 8000070: '8000070 (Delmenhorst)', 45 | 8001420: '8001420 (Detmold)', 46 | 8000080: '8000080 (Dortmund Hbf)', 47 | 8010085: '8010085 (Dresden Hbf)', 48 | 8000084: '8000084 (Düren)', 49 | 8000223: '8000223 (Düren-Annakirmespl. )', 50 | 8000085: '8000085 (Düsseldorf Hbf)', 51 | 8000086: '8000086 (Duisburg Hbf)', 52 | 8001611: '8001611 (Duisburg-Ruhrort )', 53 | 8000092: '8000092 (Elmshorn)', 54 | 8010101: '8010101 (Erfurt Hbf)', 55 | 8001844: '8001844 (Erlangen)', 56 | 8000098: '8000098 (Essen Hbf)', 57 | 8001900: '8001900 (Essen-Altenessen )', 58 | 8001920: '8001920 (Esslingen(Neckar))', 59 | 8001972: '8001972 (Feldhausen)', 60 | 8000103: '8000103 (Flensburg Hbf)', 61 | 8000105: '8000105 (Frankfurt(Main)Hbf)', 62 | 8002041: '8002041 (Frankfurt(Main)Süd)', 63 | 8010113: '8010113 (Frankfurt(Oder))', 64 | 8000107: '8000107 (Freiburg(Brsg)Hbf)', 65 | 8002078: '8002078 (Freising)', 66 | 8000112: '8000112 (Friedrichshafen St.)', 67 | 8000114: '8000114 (Fürth(Bay)Hbf)', 68 | 8000115: '8000115 (Fulda)', 69 | 8000118: '8000118 (Gelsenkirchen Hbf)', 70 | 8002224: '8002224 (Gelsenkirchen-Buer N)', 71 | 8002225: '8002225 (Gelsenkirchen-Buer S)', 72 | 8010125: '8010125 (Gera Hbf)', 73 | 8000124: '8000124 (Gießen)', 74 | 8000127: '8000127 (Göppingen)', 75 | 8000128: '8000128 (Göttingen)', 76 | 8010139: '8010139 (Greifswald)', 77 | 8002461: '8002461 (Gütersloh Hbf)', 78 | 8000142: '8000142 (Hagen Hbf)', 79 | 8010159: '8010159 (Halle(Saale)Hbf)', 80 | 8002548: '8002548 (Hamburg Dammtor)', 81 | 8000147: '8000147 (Hamburg-Harburg )', 82 | 8002549: '8002549 (Hamburg Hbf)', 83 | 8000146: '8000146 (Hamburg-Sternschanze)', 84 | 8000148: '8000148 (Hameln)', 85 | 8000149: '8000149 (Hamm (Westf.))', 86 | 8000150: '8000150 (Hanau Hbf)', 87 | 8000152: '8000152 (Hannover Hbf)', 88 | 8003487: '8003487 (HannoverMesseLaatzen)', 89 | 8000156: '8000156 (Heidelberg Hbf)', 90 | 8000157: '8000157 (Heilbronn Hbf)', 91 | 8000162: '8000162 (Herford)', 92 | 8000164: '8000164 (Herne)', 93 | 8000169: '8000169 (Hildesheim Hbf)', 94 | 8003036: '8003036 (Ibbenbüren)', 95 | 8000183: '8000183 (Ingolstadt Hbf)', 96 | 8000186: '8000186 (Iserlohn)', 97 | 8011956: '8011956 (Jena Paradies)', 98 | 8011058: '8011058 (Jena Saalbf)', 99 | 8011957: '8011957 (Jena West)', 100 | 8000189: '8000189 (Kaiserslautern Hbf)', 101 | 8000191: '8000191 (Karlsruhe Hbf)', 102 | 8000193: '8000193 (Kassel Hbf)', 103 | 8003200: '8003200 (Kassel-Wilhelmshöhe )', 104 | 8000199: '8000199 (Kiel Hbf)', 105 | 8000206: '8000206 (Koblenz Hbf)', 106 | 8000207: '8000207 (Köln Hbf)', 107 | 8003400: '8003400 (Konstanz)', 108 | 8000211: '8000211 (Krefeld Hbf)', 109 | 8000217: '8000217 (Landshut)', 110 | 8010205: '8010205 (Leipzig Hbf)', 111 | 8006713: '8006713 (Leverkusen Mitte )', 112 | 8000571: '8000571 (Lippstadt)', 113 | 8000235: '8000235 (Ludwigsburg)', 114 | 8000236: '8000236 (Ludwigshafen(Rh)Hbf)', 115 | 8003729: '8003729 (Lörrach Hbf)', 116 | 8003782: '8003782 (Lüdenscheid)', 117 | 8000237: '8000237 (Lübeck Hbf)', 118 | 8000238: '8000238 (Lüneburg)', 119 | 8000239: '8000239 (Lünen Hbf)', 120 | 8010224: '8010224 (Magdeburg Hbf)', 121 | 8000240: '8000240 (Mainz Hbf)', 122 | 8000244: '8000244 (Mannheim Hbf)', 123 | 8000337: '8000337 (Marburg(Lahn))', 124 | 8000252: '8000252 (Minden(Westf))', 125 | 8000644: '8000644 (Moers)', 126 | 8000253: '8000253 (Mönchengladbach Hbf )', 127 | 8000259: '8000259 (Mülheim(Ruhr)Hbf )', 128 | 8000261: '8000261 (München Hbf)', 129 | 8000263: '8000263 (Münster(Westf)Hbf )', 130 | 8000271: '8000271 (Neumünster)', 131 | 8000274: '8000274 (Neuss Hbf )', 132 | 8000275: '8000275 (Neustadt(Weinstr)Hbf)', 133 | 8000284: '8000284 (Nürnberg Hbf)', 134 | 8000286: '8000286 (Oberhausen Hbf )', 135 | 8000349: '8000349 (Offenbach(Main)Hbf)', 136 | 8000290: '8000290 (Offenburg)', 137 | 8000291: '8000291 (Oldenburg(Oldb))', 138 | 8000853: '8000853 (Opladen)', 139 | 8000294: '8000294 (Osnabrück Hbf)', 140 | 8000297: '8000297 (Paderborn Hbf)', 141 | 8000298: '8000298 (Passau Hbf)', 142 | 8000299: '8000299 (Pforzheim)', 143 | 8010275: '8010275 (Plauen(Vogtl) ob Bf)', 144 | 8012666: '8012666 (Potsdam Hbf)', 145 | 8004965: '8004965 (Ravensburg)', 146 | 8000307: '8000307 (Recklinghausen Hbf )', 147 | 8000309: '8000309 (Regensburg)', 148 | 8005033: '8005033 (Remscheid Hbf)', 149 | 8000314: '8000314 (Reutlingen Hbf)', 150 | 8000316: '8000316 (Rheine)', 151 | 8010304: '8010304 (Rostock Hbf)', 152 | 8000323: '8000323 (Saarbrücken Hbf)', 153 | 8005265: '8005265 (Salzgitter-Bad)', 154 | 8005269: '8005269 (Salzgitter-Immendorf)', 155 | 8005270: '8005270 (Salzgitter-Lebenstdt)', 156 | 8000325: '8000325 (Salzgitter-Ringelh.)', 157 | 8005274: '8005274 (Salzgitter-Thiede)', 158 | 8005275: '8005275 (Salzgitter-Watenst.)', 159 | 8000329: '8000329 (Schwäbisch-Gmünd)', 160 | 8005449: '8005449 (Schwäbisch Hall)', 161 | 8010324: '8010324 (Schwerin Hbf)', 162 | 8000046: '8000046 (Siegen)', 163 | 8000076: '8000076 (Soest)', 164 | 8000087: '8000087 (Solingen Hbf )', 165 | 8005628: '8005628 (Speyer Hbf)', 166 | 8000096: '8000096 (Stuttgart Hbf)', 167 | 8000134: '8000134 (Trier Hbf)', 168 | 8000141: '8000141 (Tübingen Hbf)', 169 | 8000170: '8000170 (Ulm Hbf)', 170 | 8000171: '8000171 (Unna)', 171 | 8000192: '8000192 (Wanne-Eickel Hbf )', 172 | 8010366: '8010366 (Weimar)', 173 | 8000250: '8000250 (Wiesbaden Hbf)', 174 | 8006445: '8006445 (Wilhelmshaven HBF)', 175 | 8000251: '8000251 (Witten Hbf)', 176 | 8006552: '8006552 (Wolfsburg Hbf)', 177 | 8000257: '8000257 (Worms Hbf)', 178 | 8000260: '8000260 (Würzburg Hbf)', 179 | 8000266: '8000266 (Wuppertal Hbf)', 180 | 8010397: '8010397 (Zwickau(Sachs)Hbf)' 181 | }; 182 | 183 | const DB_PRODUKTE: Record = { 184 | 1000: '1000 (City-mobil Einzelfahrt)', 185 | 1001: '1001 (City-mobil Tageskarte)', 186 | 1002: '1002 (Baden-Württemberg-Ticket)', 187 | 1004: '1004 (Baden-Württemberg-Ticket Nacht)', 188 | 1005: '1005 (Bayern-Ticket)', 189 | 1007: '1007 (Bayern-Ticket-Nacht)', 190 | 1008: '1008 (Brandenburg-Berlin-Ticket)', 191 | 1009: '1009 (Brandenburg-Berlin-Ticket-Nacht)', 192 | 1010: '1010 (Mecklenburg-Vorpommern-Ticket)', 193 | 1011: '1011 (Niedersachsen-Ticket)', // deprecated 194 | 1012: '1012 (Rheinland-Pfalz-Ticket)', 195 | 1013: '1013 (Rheinland-Pfalz-Ticket-Nacht)', 196 | 1014: '1014 (Saarland-Ticket)', 197 | 1015: '1015 (Saarland-Ticket-Nacht)', 198 | 1016: '1016 (Sachsen-Anhalt-Ticket)', 199 | 1017: '1017 (Sachsen-Ticket)', 200 | 1018: '1018 (Schleswig-Holstein-Ticket)', 201 | 1019: '1019 (Thüringen-Ticket)', 202 | 1200: '1200 (Schönes-Wochenende-Ticket)', 203 | 1201: '1201 (Quer-Durchs-Land-Ticket)', 204 | 1202: '1202 (9-Euro-Ticket)', // deprecated 205 | 1020: '1020 (Rheinland-Pfalz-Ticket + Luxemburg)', 206 | 1022: '1022 (Bayern-Böhmen-Ticket)', 207 | 1030: '1030 (Sachsen-Anhalt-Ticket plus Westharz)', 208 | 1031: '1031 (Thüringen-Ticket plus Westharz)', 209 | 1032: '1032 (Sachsen-Ticket plus Westharz)', 210 | 3000: '3000 (In-Out-System)', 211 | 9999: '9999 (Deutschlandticket)' 212 | }; 213 | 214 | export type OrgID = number; 215 | export type Produktnummer = number; 216 | export type TarifpunktNr = number; 217 | export interface VDVKAData { 218 | org_id: Record; 219 | efmprodukte: Record>; 220 | tarifpunkte: Record>; 221 | } 222 | const kaData: VDVKAData = { 223 | org_id: { 224 | 5000: '5000 (VDV E-Ticket Service)', 225 | 6262: '6262 (DB Fernverkehr)', 226 | 6263: '6263 (DB Regio Zentrale)', 227 | 6260: '6260 (DB Vertrieb GmbH)', 228 | 39030: 'TEST 39030 (DB Fernverkehr)', 229 | 39031: 'TEST 39030 (DB Regio Zentrale)', 230 | 39028: 'TEST 39028 (DB Vertrieb GmbH)' 231 | }, 232 | efmprodukte: { 233 | 6262: { 234 | 2000: '2000 (City-Ticket)' 235 | }, 236 | 39030: { 237 | 2000: '2000 (City-Ticket)' 238 | }, 239 | 6263: DB_PRODUKTE, 240 | 39031: DB_PRODUKTE 241 | }, 242 | tarifpunkte: { 243 | 5000: { 244 | 1: '1 (Bundesrepublik gesamt)', 245 | 2: '2 (Baden-Württemberg)', 246 | 3: '3 (Bayern)', 247 | 4: '4 (Berlin)', 248 | 5: '5 (Brandenburg)', 249 | 6: '6 (Bremen)', 250 | 7: '7 (Hamburg)', 251 | 8: '8 (Hessen)', 252 | 9: '9 (Mecklenburg-Vorpommern)', 253 | 10: '10 (Niedersachsen)', 254 | 11: '11 (Nordrhein-Westfalen)', 255 | 12: '12 (Rheinland-Pfalz)', 256 | 13: '13 (Saarland)', 257 | 14: '14 (Sachsen)', 258 | 15: '15 (Sachsen-Anhalt)', 259 | 16: '16 (Schleswig-Holstein)', 260 | 17: '17 (Thüringen)' 261 | }, 262 | 6262: DB_ORTE, 263 | 6263: DB_ORTE 264 | } 265 | }; 266 | 267 | export default kaData; 268 | -------------------------------------------------------------------------------- /src/postinstall/updateCerts.ts: -------------------------------------------------------------------------------- 1 | import { updateLocalCerts } from './updateLocalCerts'; 2 | updateLocalCerts(); 3 | -------------------------------------------------------------------------------- /src/postinstall/updateLocalCerts.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path'; 2 | import { readFileSync, writeFileSync } from 'fs'; 3 | import axios from 'axios'; 4 | import * as xml2js from 'xml2js'; 5 | 6 | const parser = new xml2js.Parser(); 7 | 8 | const { url, fileName } = JSON.parse(readFileSync('./cert_url.json', 'utf8')); 9 | const basePath = dirname('./cert_url.json'); 10 | export const filePath = join(basePath, fileName); 11 | 12 | export const updateLocalCerts = async (): Promise => { 13 | try { 14 | console.log(`Load public keys from ${url} ...`); 15 | const response = await axios.get(url); 16 | if (response && response.status == 200) { 17 | console.log(`Successfully loaded key file.`); 18 | } 19 | parser.parseString(response.data, function (err, result) { 20 | if (!err) { 21 | writeFileSync(filePath, JSON.stringify(result)); 22 | console.log(`Loaded ${result.keys.key.length} public keys and saved under "${filePath}".`); 23 | } else { 24 | console.log(err); 25 | } 26 | }); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_0080BL_02.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { STRING, auftraegeSBlocksV2 } from '../block-types'; 3 | 4 | const TC_0080BL_02: TicketContainerType = { 5 | name: '0080BL', 6 | version: '02', 7 | dataFields: [ 8 | { 9 | name: 'TBD0', 10 | length: 2, 11 | interpreterFn: STRING 12 | }, 13 | { 14 | name: 'blocks', 15 | length: undefined, 16 | interpreterFn: auftraegeSBlocksV2 17 | } 18 | /* # '00' bei Schönem WE-Ticket / Ländertickets / Quer-Durchs-Land 19 | # '00' bei Vorläufiger BC 20 | # '02' bei Normalpreis Produktklasse C/B, aber auch Ausnahmen 21 | # '03' bei normalem IC/EC/ICE Ticket 22 | # '04' Hinfahrt A, Rückfahrt B; Rail&Fly ABC; Veranstaltungsticket; auch Ausnahmen 23 | # '05' bei Facebook-Ticket, BC+Sparpreis+neue BC25 [Ticket von 2011] 24 | # '18' bei Kauf via Android App */ 25 | ] 26 | }; 27 | 28 | export default TC_0080BL_02; 29 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_0080BL_03.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { STRING, auftraegeSBlocksV3 } from '../block-types'; 3 | 4 | const TC_0080BL_03: TicketContainerType = { 5 | name: '0080BL', 6 | version: '03', 7 | dataFields: [ 8 | { 9 | name: 'TBD0', 10 | length: 2, 11 | interpreterFn: STRING 12 | }, 13 | { 14 | name: 'blocks', 15 | length: null, 16 | interpreterFn: auftraegeSBlocksV3 17 | } 18 | ] 19 | }; 20 | export default TC_0080BL_03; 21 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_0080ID_01.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { AUSWEIS_TYP, STRING } from '../block-types'; 3 | 4 | const TC_0080ID_01: TicketContainerType = { 5 | name: '0080ID', 6 | version: '01', 7 | dataFields: [ 8 | { 9 | name: 'ausweis_typ', 10 | length: 2, 11 | interpreterFn: AUSWEIS_TYP 12 | }, 13 | { 14 | name: 'ziffer_ausweis', 15 | length: 4, 16 | interpreterFn: STRING 17 | } 18 | ] 19 | }; 20 | export default TC_0080ID_01; 21 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_0080ID_02.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import TC_0080ID_01 from './TC_0080ID_01'; 3 | 4 | const { dataFields } = TC_0080ID_01; 5 | const TC_0080ID_02: TicketContainerType = { 6 | name: '0080ID', 7 | version: '02', 8 | dataFields 9 | }; 10 | export default TC_0080ID_02; 11 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_0080VU_01.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { INT, EFS_DATA } from '../block-types'; 3 | 4 | const TC_0080VU_01: TicketContainerType = { 5 | name: '0080VU', 6 | version: '01', 7 | dataFields: [ 8 | { 9 | name: 'Terminalnummer', 10 | length: 2, 11 | interpreterFn: INT 12 | }, 13 | { 14 | name: 'SAM_ID', 15 | length: 3, 16 | interpreterFn: INT 17 | }, 18 | { 19 | name: 'persons', 20 | length: 1, 21 | interpreterFn: INT 22 | }, 23 | { 24 | name: 'anzahlEFS', 25 | length: 1, 26 | interpreterFn: INT 27 | }, 28 | { 29 | name: 'VDV_EFS_BLOCK', 30 | length: null, 31 | interpreterFn: EFS_DATA 32 | } 33 | ] 34 | }; 35 | 36 | export default TC_0080VU_01; 37 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_1180AI_01.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { INT, STRING } from '../block-types'; 3 | 4 | const TC_1180AI_01: TicketContainerType = { 5 | name: '1180AI', 6 | version: '01', 7 | dataFields: [ 8 | { 9 | name: 'customer?', 10 | length: 7, 11 | interpreterFn: STRING 12 | }, 13 | { 14 | name: 'vorgangs_num', 15 | length: 8, 16 | interpreterFn: STRING 17 | }, 18 | { 19 | name: 'unknown1', 20 | length: 5, 21 | interpreterFn: STRING 22 | }, 23 | { 24 | name: 'unknown2', 25 | length: 2, 26 | interpreterFn: STRING 27 | }, 28 | { 29 | name: 'full_name', 30 | length: 20, 31 | interpreterFn: STRING 32 | }, 33 | { 34 | name: 'adults#', 35 | length: 2, 36 | interpreterFn: INT 37 | }, 38 | { 39 | name: 'children#', 40 | length: 2, 41 | interpreterFn: INT 42 | }, 43 | { 44 | name: 'unknown3', 45 | length: 2, 46 | interpreterFn: STRING 47 | }, 48 | { 49 | name: 'description', 50 | length: 20, 51 | interpreterFn: STRING 52 | }, 53 | { 54 | name: 'ausweis?', 55 | length: 10, 56 | interpreterFn: STRING 57 | }, 58 | { 59 | name: 'unknown4', 60 | length: 7, 61 | interpreterFn: STRING 62 | }, 63 | { 64 | name: 'valid_from', 65 | length: 8, 66 | interpreterFn: STRING 67 | }, 68 | { 69 | name: 'valid_to?', 70 | length: 8, 71 | interpreterFn: STRING 72 | }, 73 | { 74 | name: 'unknown5', 75 | length: 5, 76 | interpreterFn: STRING 77 | }, 78 | { 79 | name: 'start_bf', 80 | length: 20, 81 | interpreterFn: STRING 82 | }, 83 | { 84 | name: 'unknown6', 85 | length: 5, 86 | interpreterFn: STRING 87 | }, 88 | { 89 | name: 'ziel_bf?', 90 | length: 20, 91 | interpreterFn: STRING 92 | }, 93 | { 94 | name: 'travel_class', 95 | length: 1, 96 | interpreterFn: INT 97 | }, 98 | { 99 | name: 'unknown7', 100 | length: 6, 101 | interpreterFn: STRING 102 | }, 103 | { 104 | name: 'unknown8', 105 | length: 1, 106 | interpreterFn: STRING 107 | }, 108 | { 109 | name: 'issue_date', 110 | length: 8, 111 | interpreterFn: STRING 112 | } 113 | ] 114 | }; 115 | 116 | export default TC_1180AI_01; 117 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_U_HEAD_01.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { DB_DATETIME, HEX, STRING } from '../block-types'; 3 | 4 | const TC_U_HEAD_01: TicketContainerType = { 5 | name: 'U_HEAD', 6 | version: '01', 7 | dataFields: [ 8 | { 9 | name: 'carrier', 10 | length: 4, 11 | interpreterFn: STRING 12 | }, 13 | { 14 | name: 'auftragsnummer', 15 | length: 8, 16 | interpreterFn: STRING 17 | }, 18 | { 19 | name: 'padding', 20 | length: 12, 21 | interpreterFn: HEX 22 | }, 23 | { 24 | name: 'creation_date', 25 | length: 12, 26 | interpreterFn: DB_DATETIME 27 | }, 28 | { 29 | name: 'flags', 30 | length: 1, 31 | interpreterFn: STRING 32 | }, 33 | { 34 | name: 'language', 35 | length: 2, 36 | interpreterFn: STRING 37 | }, 38 | { 39 | name: 'language_2', 40 | length: 2, 41 | interpreterFn: STRING 42 | } 43 | ] 44 | }; 45 | export default TC_U_HEAD_01; 46 | -------------------------------------------------------------------------------- /src/ticketDataContainers/TC_U_TLAY_01.ts: -------------------------------------------------------------------------------- 1 | import { TicketContainerType } from '../TicketContainer'; 2 | import { RCT2_BLOCKS, STRING, STR_INT } from '../block-types'; 3 | 4 | const TC_U_TLAY_01: TicketContainerType = { 5 | name: 'U_TLAY', 6 | version: '01', 7 | dataFields: [ 8 | { 9 | name: 'layout', 10 | length: 4, 11 | interpreterFn: STRING 12 | }, 13 | { 14 | name: 'amount_rct2_blocks', 15 | length: 4, 16 | interpreterFn: STR_INT 17 | }, 18 | { 19 | name: 'rct2_blocks', 20 | length: null, 21 | interpreterFn: RCT2_BLOCKS 22 | } 23 | ] 24 | }; 25 | 26 | export default TC_U_TLAY_01; 27 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { FieldsType, SupportedTypes } from './FieldsType'; 2 | 3 | export type interpretFieldResult = { [index: string]: SupportedTypes }; 4 | 5 | export function interpretField(data: Buffer, fields: FieldsType[]): interpretFieldResult { 6 | let remainder = data; 7 | const res: interpretFieldResult = {}; 8 | fields.forEach((field) => { 9 | const { name, interpreterFn, length } = field; 10 | const interpreterFnDefault = (x: Buffer): Buffer => x; 11 | const interpretFunction = interpreterFn || interpreterFnDefault; 12 | 13 | if (length) { 14 | res[name] = interpretFunction(remainder.subarray(0, length)); 15 | remainder = remainder.subarray(length); 16 | } else { 17 | res[name] = interpretFunction(remainder); 18 | } 19 | }); 20 | return res; 21 | } 22 | 23 | // f is a function which returns an array with a interpreted value from data and the remaining data as the second item 24 | export type parsingFunction = (data: Buffer) => [SupportedTypes, Buffer?]; 25 | export function parseContainers(data: Buffer, f: parsingFunction): SupportedTypes[] { 26 | // f is a function which returns an array with a interpreted value from data and the remaining data as the second item 27 | let remainder = data; 28 | const containers = []; 29 | while (remainder.length > 0) { 30 | const result = f(remainder); 31 | containers.push(result[0]); 32 | remainder = result[1]; 33 | } 34 | return containers; 35 | } 36 | 37 | export function pad(number: number | string, length: number): string { 38 | let str = '' + number; 39 | while (str.length < length) { 40 | str = '0' + str; 41 | } 42 | return str; 43 | } 44 | 45 | export function handleError(error: Error): void { 46 | console.log(error); 47 | } 48 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "lib": ["ES6", "DOM"], 5 | "module": "NodeNext", 6 | "moduleResolution": "NodeNext", 7 | "outDir": "./build", 8 | "esModuleInterop": true, 9 | "importHelpers": true, 10 | "strict": true, 11 | "sourceMap": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitReturns": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitAny": true, 18 | "resolveJsonModule": true, 19 | "noImplicitThis": false, 20 | "strictNullChecks": false, 21 | "declaration": true 22 | }, 23 | "include": ["./src/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.postinstall.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "removeComments": true, 6 | "outDir": "./postinstall" 7 | }, 8 | "include": ["./src/postinstall"] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "removeComments": true 6 | }, 7 | "include": ["src/**/*"] 8 | } 9 | --------------------------------------------------------------------------------