diff --git a/others/StgCreateDocfile.empty.bin b/others/StgCreateDocfile.empty.bin new file mode 100644 index 0000000..ef7e463 Binary files /dev/null and b/others/StgCreateDocfile.empty.bin differ diff --git a/src/MsgReader.ts b/src/MsgReader.ts index 0b96f74..4f03980 100644 --- a/src/MsgReader.ts +++ b/src/MsgReader.ts @@ -33,6 +33,7 @@ export { RecurFrequency, PatternType, CalendarType, EndType, PatternTypeWeek, PatternTypeMonth, PatternTypeMonthNth, RecurrencePattern, OverrideFlags, ExceptionInfo, AppointmentRecur, } from './AppointmentRecurParser'; +export * as Rewriter from './Rewriter'; // MSG Reader implementation diff --git a/src/Rewriter.ts b/src/Rewriter.ts new file mode 100644 index 0000000..df405d6 --- /dev/null +++ b/src/Rewriter.ts @@ -0,0 +1,250 @@ +import DataStream from "./DataStream"; +import { arraysEqual } from "./utils"; +import CONST from './const' +import { TypeEnum } from "./Reader"; +import iconv from 'iconv-lite'; + +/** + * Read a block from file, located at position in the file + */ +export type ReadFile = (view: Uint8Array, position: number) => Promise; + +/** + * Write a block to file, located at position in the file + */ +export type WriteFile = (view: Uint8Array, position: number) => Promise; + +/** + * A CFBF directory entry + */ +export interface CEntry { + /** + * File index in directory entry + */ + id: number; + + /** + * Name of file or directory + */ + name: string; + + /** + * Object type + */ + type: TypeEnum; + + /** + * Not used normally + */ + clsid: Uint8Array; + + /** + * Creation time (Windows FILETIME structure, 8 bytes) + */ + creationTime: Uint8Array; + + /** + * Modified time (Windows FILETIME structure, 8 bytes) + */ + modifiedTime: Uint8Array; +} + +export interface CStore { + listEntries(parentId: number): Promise; + deleteEntry(id: number): Promise; + addFile(name: String): Promise; + addFolder(name: String): Promise; + readFile(id: number): Promise; + writeFile(id: number, readFile: ReadFile): Promise; + flush(): Promise; +} + +function subArrayOf(buf: Uint8Array, start: number, size: number): Uint8Array { + return new Uint8Array(buf.buffer, buf.byteOffset + start, size); +} + +export async function createNew(read: ReadFile, write: WriteFile): Promise { + const largeBuf = new ArrayBuffer(512 * 3); + { + const header = new Uint8Array(largeBuf, 0, 512); + const headerView = new DataView(largeBuf, 0, 512); + subArrayOf(header, CONST.HEADER.HeaderSignature, 8).set(CONST.FILE_HEADER); + subArrayOf(header, CONST.HEADER.HeaderCLSID, 16).fill(0); + headerView.setUint16(CONST.HEADER.MinorVersion, 0x003E, true); + headerView.setUint16(CONST.HEADER.MajorVersion, 0x0003, true); + headerView.setUint16(CONST.HEADER.ByteOrder, 0xFFFE, true); + headerView.setUint16(CONST.HEADER.SectorShift, 0x0009, true); + headerView.setUint16(CONST.HEADER.MiniSectorShift, 0x0006, true); + subArrayOf(header, CONST.HEADER.Reserved, 6).fill(0); + headerView.setUint32(CONST.HEADER.NumberofDirectorySectors, 0, true); + headerView.setUint32(CONST.HEADER.NumberofFATSectors, 1, true); + headerView.setUint32(CONST.HEADER.FirstDirectorySectorLocation, 1, true); + headerView.setUint32(CONST.HEADER.TransactionSignatureNumber, 0, true); + headerView.setUint32(CONST.HEADER.MiniStreamCutoffSize, 4096, true); + headerView.setUint32(CONST.HEADER.FirstMiniFATSectorLocation, 0xFFFFFFFE, true); + headerView.setUint32(CONST.HEADER.NumberofMiniFATSectors, 0, true); + headerView.setUint32(CONST.HEADER.FirstDIFATSectorLocation, 0xFFFFFFFE, true); + headerView.setUint32(CONST.HEADER.NumberofDIFATSectors, 0, true); + subArrayOf(header, CONST.HEADER.DIFAT, 436).fill(0xFF); + headerView.setUint32(CONST.HEADER.DIFAT + 4 * 0, 0, true); + + await write(new Uint8Array(headerView.buffer, headerView.byteOffset, 512), 0); + } + { + const fat = new Uint8Array(largeBuf, 512, 512); + const fatView = new DataView(largeBuf, 512, 512); + fat.fill(0xFF); // Mark as unused sectors + fatView.setUint32(4 * 0, 0xFFFFFFFD, true); // FAT sector (this) + fatView.setUint32(4 * 1, 0xFFFFFFFE, true); // Directory entry sector + await write(fat, 512); + } + { + const dir = new Uint8Array(largeBuf, 512 * 2, 512); + const dirView = new DataView(largeBuf, 512 * 2, 512); + const nameBuf = iconv.encode("Root Entry\0", 'utf16le'); + subArrayOf(dir, 0, 64).set(nameBuf); + dirView.setUint16(CONST.MSG.PROP.NAME_SIZE_OFFSET, nameBuf.byteLength, true); + dirView.setUint8(CONST.MSG.PROP.TYPE_OFFSET, CONST.MSG.PROP.TYPE_ENUM.ROOT); + dirView.setUint32(CONST.MSG.PROP.PREVIOUS_PROPERTY_OFFSET, 0xFFFFFFFF, true); + dirView.setUint32(CONST.MSG.PROP.NEXT_PROPERTY_OFFSET, 0xFFFFFFFF, true); + dirView.setUint32(CONST.MSG.PROP.CHILD_PROPERTY_OFFSET, 0xFFFFFFFF, true); + subArrayOf(dir, 0x64, 8).fill(0); + subArrayOf(dir, 0x6C, 8).fill(0); + dirView.setUint32(CONST.MSG.PROP.START_BLOCK_OFFSET, 0xFFFFFFFE, true); + dirView.setUint32(CONST.MSG.PROP.SIZE_OFFSET, 0, true); + + await write(dir, 512 * 2); + } + + return await open(read, write); +} + +export async function open(read: ReadFile, write: WriteFile): Promise { + async function mustRead(view: Uint8Array, position: number): Promise { + const readActually = await read(view, position); + if (readActually != view.byteLength) { + throw new Error('Read failed'); + } + return readActually; + } + + const headerBuf = new ArrayBuffer(512); + const header = new Uint8Array(headerBuf); + await mustRead(header, 0); + if (false + || header[0] != 0xD0 + || header[1] != 0xCF + || header[2] != 0x11 + || header[3] != 0xE0 + || header[4] != 0xA1 + || header[5] != 0xB1 + || header[6] != 0x1A + || header[7] != 0xE1 + ) { + throw new Error('Invalid header signature'); + } + const headerView = new DataView(headerBuf); + if (headerView.getUint16(CONST.HEADER.MajorVersion, true) != 3) { + throw new Error('Invalid major version'); + } + if (headerView.getUint16(CONST.HEADER.ByteOrder, true) != 0xFFFE) { + throw new Error('Invalid byte order'); + } + if (headerView.getUint16(CONST.HEADER.SectorShift, true) != 0x0009) { + throw new Error('Invalid sector size must be 512'); + } + if (headerView.getUint16(CONST.HEADER.MiniSectorShift, true) != 0x0006) { + throw new Error('Invalid mini sector size must be 64'); + } + + function createGetNextSector(): (currentSector: number) => Promise { + const tempBuf = new ArrayBuffer(512); + const tempView = new DataView(tempBuf); + const tempArray = new Uint8Array(tempBuf, 0, 4); + + return async (currentSector: number): Promise => { + // 0-126 + // 127-254 + // 255-510 + if (currentSector < 127 * 109) { + const difatIndex = (currentSector / 127) & 127; + if ((await read(tempArray, CONST.HEADER.DIFAT + 4 * difatIndex)) != 4) { + throw new Error('Read failed'); + } + const fatSector = tempView.getUint32(0, true); + if (fatSector == 0xFFFFFFFE) { + throw new Error('End of chain'); + } + const fatIndex = currentSector % 127; + if ((await read(tempArray, 512 * (1 + fatSector) + 4 * fatIndex)) != 4) { + throw new Error('Read failed'); + } + return tempView.getUint32(0, true); + } + else { + throw new Error('Not implemented'); + } + }; + } + + async function readOfSector(firstSector: number, position: number, buf: Uint8Array): Promise { + let remaining = buf.byteLength; + let offset = 0; + let currentSector = firstSector; + const getNextSector = createGetNextSector(); + while (1 <= remaining) { + if (position < 512) { + const readSize = Math.min(512 - position, remaining); + await mustRead( + new Uint8Array( + buf.buffer, + buf.byteOffset + offset, + readSize + ), + 512 * (1 + currentSector) + ); + remaining -= readSize; + offset += readSize; + } + + currentSector = await getNextSector(currentSector); + + position -= 512; + } + } + + return { + async listEntries(parentId: number): Promise { + const tempBuf = new ArrayBuffer(128); + const temp4 = new Uint8Array(tempBuf, 0, 4); + const temp128 = new Uint8Array(tempBuf, 0, 128); + const tempView = new DataView(tempBuf); + await mustRead(temp4, CONST.HEADER.FirstDirectorySectorLocation); + const firstSector = tempView.getUint32(0, true); + await readOfSector(firstSector, 128 * parentId + CONST.MSG.PROP.CHILD_PROPERTY_OFFSET, temp4); + const childIndex = tempView.getUint32(0, true); + const list: CEntry[] = []; + + await readOfSector(firstSector, 128 * childIndex, temp128); + + return list; + }, + async deleteEntry(id: number): Promise { + }, + async addFile(name: String): Promise { + return 0; + }, + async addFolder(name: String): Promise { + return 0; + }, + async readFile(id: number): Promise { + return async (view: Uint8Array, position: number) => { + return 0; + }; + }, + async writeFile(id: number, readFile: ReadFile): Promise { + }, + async flush(): Promise { + }, + }; +} diff --git a/src/const.ts b/src/const.ts index 4fbc06d..fff7cea 100644 --- a/src/const.ts +++ b/src/const.ts @@ -189,5 +189,25 @@ export default { INNER_MSG: '000d' } } - } + }, + HEADER: { + HeaderSignature: 0, + HeaderCLSID: 8, + MinorVersion: 24, + MajorVersion: 26, + ByteOrder: 28, + SectorShift: 30, + MiniSectorShift: 32, + Reserved: 34, + NumberofDirectorySectors: 40, + NumberofFATSectors: 44, + FirstDirectorySectorLocation: 48, + TransactionSignatureNumber: 52, + MiniStreamCutoffSize: 56, + FirstMiniFATSectorLocation: 60, + NumberofMiniFATSectors: 64, + FirstDIFATSectorLocation: 68, + NumberofDIFATSectors: 72, + DIFAT: 76, + }, } \ No newline at end of file diff --git a/test/test.js b/test/test.js index 9924c4c..72f9917 100644 --- a/test/test.js +++ b/test/test.js @@ -622,7 +622,6 @@ describe('MsgReader', function () { }); - describe('Burner', function () { const burn = require('../lib/Burner').burn; const Reader = require('../lib/Reader').Reader; @@ -843,6 +842,21 @@ describe('Burner', function () { }); }); +describe('Rewriter', function () { + const { Rewriter } = require('../lib/MsgReader'); + it('createNew', async function () { + const file = await fs.promises.open("C:/A/a.msg", "r+"); + const filesys = await Rewriter.createNew( + async (view, position) => { + return (await file.read(view, 0, view.byteLength, position)).bytesRead; + }, + async (view, position) => { + await file.write(view, 0, view.byteLength, position); + } + ); + }); +}); + describe('toHexStr', function () { const toHexStr = require('../lib/utils').toHexStr; it('tests', function () { diff --git a/uml/rewrite.plantuml b/uml/rewrite.plantuml new file mode 100644 index 0000000..3862683 --- /dev/null +++ b/uml/rewrite.plantuml @@ -0,0 +1,41 @@ +@startuml class + +interface ReadFile { + +readFile(view: Uint8Array, position: number): Promise +} + +interface WriteFile { + +writeFile(view: Uint8Array, position: number): Promise +} + +ReadFile --|> CfbfUtil +WriteFile --|> CfbfUtil + +class CfbfUtil { + +createNew(readFile: ReadFile, writeFile: WriteFile): Promise + +open(readFile: ReadFile, writeFile: WriteFile): Promise +} + +CfbfUtil --|> CStore + +interface CEntry { + +id: number + +name: String + +type: number + +clsid: Uint8Array + +creationTime: Uint8Array + +modifiedTime: Uint8Array +} + +CStore --|> CEntry + +interface CStore { + +listEntries(parentId: number): Promise + +deleteEntry(id: number): Promise + +addFile(name: String): Promise + +addFolder(name: String): Promise + +readFile(id: number): Promise + +writeFile(id: number, readFile: ReadFile): Promise +} + +@enduml