diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cebf254 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.gba +*.sav +*.sgm +*.iml +.idea +node_modules \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..141c535 --- /dev/null +++ b/index.js @@ -0,0 +1,12 @@ +#! /usr/bin/env node +const { program } = require('commander') +const {applyPatch} = require('./js/cmd') +program + .command('patch') + .description('List all the TODO tasks') + .argument('', 'the patch to apply') + .argument('','the rom file getting the patch') + .option('-v, --validate-checksum','should validate checksum') + .action((patch, romFile, options) => applyPatch(romFile, patch, options)); + +program.parse() \ No newline at end of file diff --git a/js/File.js b/js/File.js new file mode 100644 index 0000000..bb0d23b --- /dev/null +++ b/js/File.js @@ -0,0 +1,12 @@ +const path = require('path'); +const fs = require("fs"); + +exports.File = File; + +function File(_path) { + this.stat = fs.statSync(_path); + this.size = this.stat.size; + this.name = path.basename(_path); + this.type = this.stat.type; + this.data = fs.readFileSync(_path); +} \ No newline at end of file diff --git a/js/MarcFile.js b/js/MarcFile.js index ec86c08..f12be87 100644 --- a/js/MarcFile.js +++ b/js/MarcFile.js @@ -1,6 +1,11 @@ /* MODDED VERSION OF MarcFile.js v20230202 - Marc Robledo 2014-2023 - http://www.marcrobledo.com/license */ -function MarcFile(source, onLoad){ +if(typeof module !== "undefined" && module.exports){ + exports.MarcFile = MarcFile; + // saveAs = (blob,fileName) => fs.writeFileSync(fileName, blob.arrayBuffer()) +} + +function MarcFile(source, onLoad){ if(typeof source==='object' && source.files) /* get first file only if source is input with multiple files */ source=source.files[0]; @@ -9,27 +14,34 @@ function MarcFile(source, onLoad){ this._lastRead=null; if(typeof source==='object' && source.name && source.size){ /* source is file */ - if(typeof window.FileReader!=='function') - throw new Error('Incompatible Browser'); - this.fileName=source.name; this.fileType=source.type; this.fileSize=source.size; - - this._fileReader=new FileReader(); - this._fileReader.marcFile=this; - this._fileReader.addEventListener('load',function(){ - this.marcFile._u8array=new Uint8Array(this.result); - this.marcFile._dataView=new DataView(this.result); - - if(onLoad) - onLoad.call(); - },false); - - this._fileReader.readAsArrayBuffer(source); - - - + try { + if (typeof window.FileReader !== 'function') + throw new Error('Incompatible Browser'); + this._fileReader=new FileReader(); + this._fileReader.addEventListener('load',function(){ + this.marcFile._u8array=new Uint8Array(this.result); + this.marcFile._dataView=new DataView(this.result); + + if(onLoad) + onLoad.call(); + },false); + + + this._fileReader.marcFile=this; + + this._fileReader.readAsArrayBuffer(source); + }catch (e){ + if(!(e instanceof ReferenceError)) + throw e; + this._fileReader = {} + this._u8array = new Uint8Array(source.data.buffer); + this._dataView=new DataView(source.data.buffer); + this.source = source; + MarcFile.prototype.readString = () => source.data.toString(); + } }else if(typeof source==='object' && typeof source.fileName==='string' && typeof source.littleEndian==='boolean'){ /* source is MarcFile */ this.fileName=source.fileName; this.fileType=source.fileType; @@ -127,22 +139,26 @@ MarcFile.prototype.copyToFile=function(target, offsetSource, len, offsetTarget){ MarcFile.prototype.save=function(){ - var blob; - try{ - blob=new Blob([this._u8array],{type:this.fileType}); - }catch(e){ - //old browser, use BlobBuilder - window.BlobBuilder=window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; - if(e.name==='InvalidStateError' && window.BlobBuilder){ - var bb=new BlobBuilder(); - bb.append(this._u8array.buffer); - blob=bb.getBlob(this.fileType); - }else{ - throw new Error('Incompatible Browser'); - return false; + if(typeof module !== "undefined" && module.exports) + require('fs').writeFileSync(this.fileName, Buffer.from(this._u8array.buffer)); + else { + var blob; + try { + blob = new Blob([this._u8array], {type: this.fileType}); + } catch (e) { + //old browser, use BlobBuilder + window.BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; + if (e.name === 'InvalidStateError' && window.BlobBuilder) { + var bb = new BlobBuilder(); + bb.append(this._u8array.buffer); + blob = bb.getBlob(this.fileType); + } else { + throw new Error('Incompatible Browser'); + return false; + } } + saveAs(blob, this.fileName); } - saveAs(blob,this.fileName); } diff --git a/js/cmd.js b/js/cmd.js new file mode 100644 index 0000000..c3f4701 --- /dev/null +++ b/js/cmd.js @@ -0,0 +1,193 @@ +const {MarcFile} = require("./MarcFile"); +const {File} = require("./File") +const {IPS_MAGIC, parseIPSFile} = require("./formats/ips") +const {UPS_MAGIC, parseUPSFile} = require("./formats/ups") +const {APS_N64_MAGIC, parseAPSFile} = require("./formats/aps_n64") +const {APS_GBA_MAGIC, APSGBA} = require("./formats/aps_gba") +const {BPS_MAGIC, parseBPSFile} = require("./formats/bps") +const {RUP_MAGIC, parseRUPFile} = require("./formats/rup") +const {PPF_MAGIC, parsePPFFile} = require("./formats/ppf") +const {PMSR_MAGIC, parseMODFile} = require("./formats/pmsr") +const {VCDIFF_MAGIC, parseVCDIFF} = require("./formats/vcdiff") +const {ZIP_MAGIC, ZIPManager} = require("./formats/zip") +const {md5} = require("./crc") + +function hasHeader(romFile){ + if(romFile.fileSize<=0x600200){ + if(romFile.fileSize%1024===0) + return 0; + + for(var i=0; i ' + padZeroes(checksumInfo.calculated) + ')')) { + // checksumInfo.fix(patchedRom); + // } + // } + + + console.log('apply'); + patchedRom.save(); +} + + + +function padZeroes(intVal, nBytes){ + var hexString=intVal.toString(16); + while(hexString.length>>1)):(c>>>1)); + crcTable[n]=c; + } + return crcTable; +}()); +function crc32(marcFile, headerSize, ignoreLast4Bytes){ + var data=headerSize? new Uint8Array(marcFile._u8array.buffer, headerSize):marcFile._u8array; + + var crc=0^(-1); + + var len=ignoreLast4Bytes?data.length-4:data.length; + for(var i=0;i>>8)^CRC32_TABLE[(crc^data[i])&0xff]; + + return ((crc^(-1))>>>0); +} +function updateChecksums(file, romFile, startOffset, force){ + if(file===romFile && file.fileSize>33554432 && !force){ + console.log('File is too big. Force calculate checksum? Add -f to command'); + return false; + } + + console.log('crc32='+padZeroes(crc32(file, startOffset), 4)); + console.log('md5='+padZeroes(md5(file, startOffset), 16)); +} +let headerSize; + +function canHaveFakeHeader(romFile){ + if(romFile.fileSize<=0x600000){ + for(let i=0; i { + rom = new MarcFile(new File(rom)); + _parseROM(rom); + patch = new MarcFile(new File(patch)); + patch = _readPatchFile(patch, rom); + if (!patch || !rom) + throw new Error('No ROM/patch selected'); + console.log('apply'+ 'applying_patch'+ 'loading'); + + try { + preparePatchedRom(rom, patch.apply(rom, validateChecksums), headerSize); + } catch (e) { + // console.log('apply'+ 'Error: ' + (e.message)+ 'error'); + throw e; + } +} +module.exports = {applyPatch} \ No newline at end of file diff --git a/js/crc.js b/js/crc.js index 4c8240d..2a239d5 100644 --- a/js/crc.js +++ b/js/crc.js @@ -124,3 +124,7 @@ function crc16(marcFile, offset, len){ return crc & 0xffff; } + +if(typeof module !== "undefined" && module.exports){ + module.exports = {md5}; +} \ No newline at end of file diff --git a/js/formats/aps_gba.js b/js/formats/aps_gba.js index cb1f43b..69ec897 100644 --- a/js/formats/aps_gba.js +++ b/js/formats/aps_gba.js @@ -4,7 +4,9 @@ const APS_GBA_MAGIC='APS1'; const APS_GBA_BLOCK_SIZE=0x010000; //64Kb const APS_GBA_RECORD_SIZE=4 + 2 + 2 + APS_GBA_BLOCK_SIZE; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {APS_GBA_MAGIC, APSGBA}; +} function APSGBA(){ this.sourceSize=0; this.targetSize=0; diff --git a/js/formats/aps_n64.js b/js/formats/aps_n64.js index b51efc2..bb8ecde 100644 --- a/js/formats/aps_n64.js +++ b/js/formats/aps_n64.js @@ -5,7 +5,9 @@ const APS_N64_MAGIC='APS10'; const APS_RECORD_RLE=0x0000; const APS_RECORD_SIMPLE=0x01; const APS_N64_MODE=0x01; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {APS_N64_MAGIC, parseAPSFile}; +} function APS(){ this.records=[]; this.headerType=0; diff --git a/js/formats/bps.js b/js/formats/bps.js index 839839d..f402638 100644 --- a/js/formats/bps.js +++ b/js/formats/bps.js @@ -6,7 +6,9 @@ const BPS_ACTION_SOURCE_READ=0; const BPS_ACTION_TARGET_READ=1; const BPS_ACTION_SOURCE_COPY=2; const BPS_ACTION_TARGET_COPY=3; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {BPS_MAGIC, parseBPSFile}; +} function BPS(){ this.sourceSize=0; diff --git a/js/formats/ips.js b/js/formats/ips.js index 853ad6e..4ff5df8 100644 --- a/js/formats/ips.js +++ b/js/formats/ips.js @@ -1,13 +1,17 @@ /* IPS module for Rom Patcher JS v20220417 - Marc Robledo 2016-2022 - http://www.marcrobledo.com/license */ /* File format specification: http://www.smwiki.net/wiki/IPS_file_format */ - - const IPS_MAGIC='PATCH'; const IPS_MAX_SIZE=0x1000000; //16 megabytes const IPS_RECORD_RLE=0x0000; const IPS_RECORD_SIMPLE=0x01; +if(typeof module !== "undefined" && module.exports){ + module.exports = {IPS_MAGIC, IPS_MAX_SIZE, IPS_RECORD_RLE, IPS_RECORD_SIMPLE, IPS, parseIPSFile}; +} + + + function IPS(){ this.records=[]; this.truncate=false; diff --git a/js/formats/pmsr.js b/js/formats/pmsr.js index e4ba2e5..3986c72 100644 --- a/js/formats/pmsr.js +++ b/js/formats/pmsr.js @@ -5,7 +5,9 @@ const PMSR_MAGIC='PMSR'; const YAY0_MAGIC='Yay0'; const PAPER_MARIO_USA10_CRC32=0xa7f5cd7e; const PAPER_MARIO_USA10_FILE_SIZE=41943040; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {PMSR_MAGIC, parseMODFile}; +} function PMSR(){ this.targetSize=0; diff --git a/js/formats/ppf.js b/js/formats/ppf.js index cf919d9..d3e0fc7 100644 --- a/js/formats/ppf.js +++ b/js/formats/ppf.js @@ -2,12 +2,14 @@ /* File format specification: https://www.romhacking.net/utilities/353/ */ - const PPF_MAGIC='PPF'; const PPF_IMAGETYPE_BIN=0x00; const PPF_IMAGETYPE_GI=0x01; const PPF_BEGIN_FILE_ID_DIZ_MAGIC='@BEG';//@BEGIN_FILE_ID.DIZ +if(typeof module !== "undefined" && module.exports){ + module.exports = {PPF_MAGIC, parsePPFFile}; +} function PPF(){ this.version=3; this.imageType=PPF_IMAGETYPE_BIN; diff --git a/js/formats/rup.js b/js/formats/rup.js index b123113..8d9f59a 100644 --- a/js/formats/rup.js +++ b/js/formats/rup.js @@ -6,7 +6,9 @@ const RUP_COMMAND_END=0x00; const RUP_COMMAND_OPEN_NEW_FILE=0x01; const RUP_COMMAND_XOR_RECORD=0x02; const RUP_ROM_TYPES=['raw','nes','fds','snes','n64','gb','sms','mega','pce','lynx']; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {RUP_MAGIC, parseRUPFile}; +} function RUP(){ this.author=''; diff --git a/js/formats/ups.js b/js/formats/ups.js index 9c5a8dd..46a6055 100644 --- a/js/formats/ups.js +++ b/js/formats/ups.js @@ -2,7 +2,9 @@ /* File format specification: http://www.romhacking.net/documents/392/ */ const UPS_MAGIC='UPS1'; - +if(typeof module !== "undefined" && module.exports){ + module.exports = {UPS_MAGIC, UPS, parseUPSFile}; +} function UPS(){ this.records=[]; this.sizeInput=0; diff --git a/js/formats/vcdiff.js b/js/formats/vcdiff.js index b0584f2..485f1c1 100644 --- a/js/formats/vcdiff.js +++ b/js/formats/vcdiff.js @@ -6,7 +6,6 @@ some code and ideas borrowed from: https://hack64.net/jscripts/libpatch.js?6 */ - //const VCDIFF_MAGIC=0xd6c3c400; const VCDIFF_MAGIC='\xd6\xc3\xc4'; /* @@ -17,7 +16,10 @@ const XDELTA_100_MAGIC='%XDZ002'; const XDELTA_104_MAGIC='%XDZ003'; const XDELTA_110_MAGIC='%XDZ004'; */ - +if(typeof module !== "undefined" && module.exports){ + module.exports = {VCDIFF_MAGIC, parseVCDIFF}; + MarcFile = require("../MarcFile").MarcFile; +} function VCDIFF(patchFile){ this.file=patchFile; diff --git a/js/formats/zip.js b/js/formats/zip.js index f5c28f0..3ebdd39 100644 --- a/js/formats/zip.js +++ b/js/formats/zip.js @@ -2,6 +2,9 @@ const ZIP_MAGIC='\x50\x4b\x03\x04'; +if(typeof module !== "undefined" && module.exports){ + module.exports = {ZIP_MAGIC, ZIPManager}; +} var ZIPManager=(function(){ const FILTER_PATCHES=/\.(ips|ups|bps|aps|rup|ppf|mod|xdelta|vcdiff)$/i; //const FILTER_ROMS=/(?=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/conf": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/conf/-/conf-11.0.1.tgz", + "integrity": "sha512-WlLiQboEjKx0bYx2IIRGedBgNjLAxtwPaCSnsjWPST5xR0DB4q8lcsO/bEH9ZRYNcj63Y9vj/JG/5Fg6uWzI0Q==", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", + "atomically": "^2.0.0", + "debounce-fn": "^5.1.2", + "dot-prop": "^7.2.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.3.8" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-5.1.2.tgz", + "integrity": "sha512-Sr4SdOZ4vw6eQDvPYNxHogvrxmCIld/VenC5JbNrFwMiwd7lY/Z18ZFfo+EWNG4DD9nFlAujWAo/wGuOPHmy5A==", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", + "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", + "dependencies": { + "type-fest": "^2.11.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/json-schema-typed": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", + "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stubborn-fs": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.4.tgz", + "integrity": "sha512-KRa4nIRJ8q6uApQbPwYZVhOof8979fw4xbajBWa5kPJFa4nyY3aFaMWVyIVCDnkNCCG/3HLipUZ4QaNlYsmX1w==" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/when-exit": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.0.tgz", + "integrity": "sha512-H85ulNwUBU1e6PGxkWUDgxnbohSXD++ah6Xw1VHAN7CtypcbZaC4aYjQ+C2PMVaDkURDuOinNAT+Lnz3utWXxQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ed2b7eb --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "chalk": "^5.2.0", + "commander": "^11.0.0", + "conf": "^11.0.1" + }, + "name": "rompatcher", + "description": "A ROM patcher made in HTML5.", + "version": "1.0.0", + "main": "index.js", + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT" +}