diff --git a/.gitignore b/.gitignore index ab45ecb..560db17 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ yarn-error.log* .npm .idea .DS_Store +dist/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b89bab0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d8d5510 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +##### 1.1.0 + +* Fix possible incorrect share code string detection +* TypeScript support + +##### 1.0.0 + +Initial release diff --git a/README.md b/README.md index 21b40dd..a08da01 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Node module to decode / encode the CSGO share codes used to share game replays b # Installation -`npm install csgo-sharecode` +`npm install csgo-sharecode` or `yarn install csgo-sharecode` + +TypeScript definitions provided. # Usage diff --git a/example.js b/example.js index a919189..e66500b 100644 --- a/example.js +++ b/example.js @@ -1,8 +1,10 @@ // const ShareCode = require('csgo-sharecode'); // OR using ES modules // import ShareCode from 'csgo-sharecode'; -const ShareCode = require('./index'); - +// import { encode, decode } from 'csgo-sharecode'; +// When using TS you can import the ShareCode (object representation of a full share code) and TwoComplementInteger64 types +// import { ShareCode, TwoComplementInteger64 } from 'csgo-sharecode'; +const ShareCode = require('./dist') // sample example: // share code: CSGO-GADqf-jjyJ8-cSP2r-smZRo-TO2xK @@ -20,13 +22,13 @@ const match = { low: 143, }, tvPort: 599906796, -}; +} -console.log('encoding share code:\n', match); -const code = ShareCode.encode(match.matchId, match.reservationId, match.tvPort); -console.log('result:\n', code); +console.log('encoding share code:\n', match) +const code = ShareCode.encode(match.matchId, match.reservationId, match.tvPort) +console.log('result:\n', code) -const shareCode = 'CSGO-GADqf-jjyJ8-cSP2r-smZRo-TO2xK'; -console.log('decoding share code:\n', shareCode); -const info = ShareCode.decode(shareCode); -console.log('result:\n', info); +const shareCode = 'CSGO-GADqf-jjyJ8-cSP2r-smZRo-TO2xK' +console.log('decoding share code:\n', shareCode) +const info = ShareCode.decode(shareCode) +console.log('result:\n', info) diff --git a/index.js b/index.js deleted file mode 100644 index 4492007..0000000 --- a/index.js +++ /dev/null @@ -1,147 +0,0 @@ -'use strict'; - -const BigNumber = require('bignumber.js'); - -const DICTIONARY = 'ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789'; -const DICTIONARY_LENGTH = DICTIONARY.length; -const SHARECODE_PATTERN = /CSGO(-?[\w]{5}){5}/; - -/** - * Convert a byte array into a hexadecimal string - * - * @param bytes - * @returns {string} - */ -function bytesToHex(bytes) { - return Array.from(bytes, (byte) => { - return ('0' + (byte & 0xff).toString(16)).slice(-2); - }).join(''); -} - -/** - * Convert a hexadecimal string into a byte array - * - * @param str - * @returns {Array} - */ -function hexToBytes(str) { - let array = []; - for (var i = 0, j = 0; i < str.length; i += 2, j++) { - array[j] = parseInt('0x' + str.substr(i, 2)); - } - - return array; -} - -/** - * Convert a 64 bit 2 complement integer into a byte array (big endian byte representation) - * - * @param high - * @param low - * @returns Array - */ -function longToBytesBE(high, low) { - return [ - (high >>> 24) & 0xff, - (high >>> 16) & 0xff, - (high >>> 8) & 0xff, - (high & 0xff), - (low >>> 24) & 0xff, - (low >>> 16) & 0xff, - (low >>> 8) & 0xff, - (low & 0xff) - ]; -} - -/** - * Convert an int into a byte array (low bits only) - * - * @param number - * @returns Array - */ -function int16ToBytes(number) { - return [ - (number & 0x0000ff00) >> 8, - (number & 0x000000ff) - ]; -} - -/** - * Convert a byte array into an int32 - * - * @param bytes - * @returns {number} - */ -function bytesToInt32(bytes) { - let number = 0; - for (let i = 0; i < bytes.length; i++) { - number += bytes[i]; - if (i < bytes.length - 1) { - number = number << 8; - } - } - - return number; -} - -module.exports = { - /** - * Encode a share code from its object data and return its string representation. - * Required fields should come from a CDataGCCStrike15_v2_MatchInfo protobuf message. - * https://github.com/SteamRE/SteamKit/blob/master/Resources/Protobufs/csgo/cstrike15_gcmessages.proto#L773 - * @param matchId {Object|Long} match_id - * @param reservationId {Object|Long} reservation_id - * @param tvPort number tv_port - * @return {string} Share code as string - */ - encode: (matchId, reservationId, tvPort) => { - const matchBytes = longToBytesBE(matchId.high, matchId.low).reverse(); - const reservationBytes = longToBytesBE(reservationId.high, reservationId.low).reverse(); - const tvBytes = int16ToBytes(tvPort).reverse(); - const bytes = Array.prototype.concat(matchBytes, reservationBytes, tvBytes); - const bytesHex = bytesToHex(bytes); - let total = new BigNumber(bytesHex, 16); - - let c = ''; - let rem = 0; - for (let i = 0; i < 25; i++) { - rem = total.mod(DICTIONARY_LENGTH); - c += DICTIONARY[rem.floor()]; - total = total.div(DICTIONARY_LENGTH); - } - - return `CSGO-${c.substr(0, 5)}-${c.substr(5, 5)}-${c.substr(10, 5)}-${c.substr(15, 5)}-${c.substr(20, 5)}`; - }, - /** - * Decode a CSGO share code from its string and return its object data representation. - * Share code format excepted: CSGO-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx - * @param shareCode Share code as string - * @return {{matchId: {low: number, high: number}, reservationId: {low: number, high: number}, tvPort: number}} - */ - decode: (shareCode) => { - if (!shareCode.match(SHARECODE_PATTERN)) { - throw new Error('Invalid share code'); - } - - shareCode = shareCode.replace(/CSGO|-/g, ''); - shareCode = Array.from(shareCode).reverse(); - let big = new BigNumber(0); - for (let i = 0; i < shareCode.length; i++) { - big = big.mul(DICTIONARY_LENGTH).add(DICTIONARY.indexOf(shareCode[i])); - } - - const bytes = hexToBytes(big.toString(16)); - - return { - matchId: { - low: bytesToInt32(bytes.slice(0, 4).reverse()), - high: bytesToInt32(bytes.slice(4, 8).reverse()), - }, - reservationId: { - low: bytesToInt32(bytes.slice(8, 12).reverse()), - high: bytesToInt32(bytes.slice(12, 16).reverse()), - }, - tvPort: bytesToInt32(bytes.slice(16, 18).reverse()), - }; - } -}; diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..cfc20c3 --- /dev/null +++ b/index.ts @@ -0,0 +1,143 @@ +import BigNumber from 'bignumber.js' + +const DICTIONARY = 'ABCDEFGHJKLMNOPQRSTUVWXYZabcdefhijkmnopqrstuvwxyz23456789' +const DICTIONARY_LENGTH = DICTIONARY.length +const SHARECODE_PATTERN = /CSGO(-?[\w]{5}){5}$/ + +export interface TwoComplementInteger64 { + low: number + high: number + unsigned?: boolean +} + +export interface Sharecode { + matchId: TwoComplementInteger64 + reservationId: TwoComplementInteger64 + tvPort: number +} + +/** + * Convert a byte array into a hexadecimal string + */ +function bytesToHex(bytes: number[]) { + return Array.from(bytes, byte => { + return ('0' + (byte & 0xff).toString(16)).slice(-2) + }).join('') +} + +/** + * Convert a hexadecimal string into a byte array + */ +function hexToBytes(str: string) { + let array = [] + for (var i = 0, j = 0; i < str.length; i += 2, j++) { + array[j] = parseInt('0x' + str.substr(i, 2)) + } + + return array +} + +/** + * Convert a 64 bit 2 complement integer into a byte array (big endian byte representation) + */ +function longToBytesBE(high: number, low: number) { + return [ + (high >>> 24) & 0xff, + (high >>> 16) & 0xff, + (high >>> 8) & 0xff, + high & 0xff, + (low >>> 24) & 0xff, + (low >>> 16) & 0xff, + (low >>> 8) & 0xff, + low & 0xff, + ] +} + +/** + * Convert an int into a byte array (low bits only) + */ +function int16ToBytes(number: number) { + return [(number & 0x0000ff00) >> 8, number & 0x000000ff] +} + +/** + * Convert a byte array into an int32 + */ +function bytesToInt32(bytes: number[]) { + let number = 0 + for (let i = 0; i < bytes.length; i++) { + number += bytes[i] + if (i < bytes.length - 1) { + number = number << 8 + } + } + + return number +} + +/** + * Encode a share code from its ShareCode object type and return its string representation. + * Required fields should come from a CDataGCCStrike15_v2_MatchInfo protobuf message. + * https://github.com/SteamRE/SteamKit/blob/master/Resources/Protobufs/csgo/cstrike15_gcmessages.proto#L785 + */ +const encode = ( + matchId: TwoComplementInteger64, + reservationId: TwoComplementInteger64, + tvPort: number +): string => { + const matchBytes = longToBytesBE(matchId.high, matchId.low).reverse() + const reservationBytes = longToBytesBE( + reservationId.high, + reservationId.low + ).reverse() + const tvBytes = int16ToBytes(tvPort).reverse() + const bytes = Array.prototype.concat(matchBytes, reservationBytes, tvBytes) + const bytesHex = bytesToHex(bytes) + let total = new BigNumber(bytesHex, 16) + + let c = '' + let rem: BigNumber = new BigNumber(0) + for (let i = 0; i < 25; i++) { + rem = total.mod(DICTIONARY_LENGTH) + c += DICTIONARY[rem.integerValue(BigNumber.ROUND_FLOOR).toNumber()] + total = total.div(DICTIONARY_LENGTH) + } + + return `CSGO-${c.substr(0, 5)}-${c.substr(5, 5)}-${c.substr( + 10, + 5 + )}-${c.substr(15, 5)}-${c.substr(20, 5)}` +} + +/** + * Decode a CSGO share code from its string and return it as a ShareCode object type. + * Share code format excepted: CSGO-xxxxx-xxxxx-xxxxx-xxxxx-xxxxx + */ +const decode = (shareCode: string): Sharecode => { + if (!shareCode.match(SHARECODE_PATTERN)) { + throw new Error('Invalid share code') + } + + shareCode = shareCode.replace(/CSGO|-/g, '') + const chars = Array.from(shareCode).reverse() + let big = new BigNumber(0) + for (let i = 0; i < chars.length; i++) { + big = big.multipliedBy(DICTIONARY_LENGTH).plus(DICTIONARY.indexOf(chars[i])) + } + + const bytes = hexToBytes(big.toString(16)) + + return { + matchId: { + low: bytesToInt32(bytes.slice(0, 4).reverse()), + high: bytesToInt32(bytes.slice(4, 8).reverse()), + }, + reservationId: { + low: bytesToInt32(bytes.slice(8, 12).reverse()), + high: bytesToInt32(bytes.slice(12, 16).reverse()), + }, + tvPort: bytesToInt32(bytes.slice(16, 18).reverse()), + } +} + +export { encode, decode } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27fdac6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "csgo-sharecode", + "version": "1.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "bignumber.js": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-7.2.1.tgz", + "integrity": "sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ==" + }, + "prettier": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.2.tgz", + "integrity": "sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg==", + "dev": true + }, + "typescript": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.1.tgz", + "integrity": "sha512-zQIMOmC+372pC/CCVLqnQ0zSBiY7HHodU7mpQdjiZddek4GMj31I3dUJ7gAs9o65X7mnRma6OokOkc6f9jjfBg==", + "dev": true + } + } +} diff --git a/package.json b/package.json index ea9f692..f64940c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "csgo-sharecode", - "version": "1.0.1", + "version": "1.1.0", "description": "Encode / decode CSGO share code", - "main": "index.js", + "main": "dist/index.js", "author": { "name": "AkiVer", "email": "ys.renaud@gmail.com" @@ -12,7 +12,7 @@ "type": "git", "url": "https://github.com/akiver/csgo-sharecode.git" }, - "homepage": "http://github.com/akiver/csgo-sharecode", + "homepage": "https://github.com/akiver/csgo-sharecode", "keywords": [ "csgo", "valve", @@ -20,10 +20,23 @@ "replays", "demos" ], + "files": [ + "dist", + "index.ts", + "index.d.ts" + ], + "scripts": { + "build": "npx tsc", + "dev": "npx tsc --watch" + }, "dependencies": { - "bignumber.js": "^4.0.4" + "bignumber.js": "^7.2.1" }, "engines": { "node": ">=4.0.0" + }, + "devDependencies": { + "prettier": "^1.14.2", + "typescript": "^3.0.1" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..88e8ccd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "declaration": true, + "lib": ["es5", "es6"], + "noImplicitAny": true, + "noUnusedLocals": true, + "outDir": "./dist", + "sourceMap": true, + "strictNullChecks": true, + "target": "es5", + }, + "files": [ + "index.ts", + ] +} \ No newline at end of file