Skip to content

Commit

Permalink
feat(parser): 배포용 문서 지원
Browse files Browse the repository at this point in the history
Co-authored-by: Karam KIM <zntuszntus@gmail.com>
  • Loading branch information
hahnlee and zntus committed May 23, 2024
1 parent f467b66 commit d4ac55b
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 10 deletions.
2 changes: 2 additions & 0 deletions packages/parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
},
"devDependencies": {
"@hwp.js/eslint-config": "workspace:*",
"@types/aes-js": "^3.1.4",
"@types/node": "^20",
"@types/pako": "^1.0.1",
"eslint": "^8",
Expand All @@ -45,6 +46,7 @@
"typescript": "^5"
},
"dependencies": {
"aes-js": "^3.1.2",
"cfb": "^1.2.2",
"pako": "^2.1.0"
}
Expand Down
7 changes: 1 addition & 6 deletions packages/parser/src/models/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,7 @@ export class HWPDocument {
const sections: Section[] = []

for (let i = 0; i < docInfo.properties.sections; i += 1) {
const entry = find(container, `Root Entry/BodyText/Section${i}`)

if (!entry) {
throw new Error('Section not exist')
}
sections.push(Section.fromEntry(entry, header, options))
sections.push(Section.fromCFB(container, i, header, options))
}

const binDataList: BinData[] = []
Expand Down
99 changes: 95 additions & 4 deletions packages/parser/src/models/section.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@
* limitations under the License.
*/

import type { CFB$Entry } from 'cfb'
import { find, type CFB$Container, type CFB$Entry } from 'cfb'
import { inflate } from 'pako'
import * as aesjs from 'aes-js'

import type { HWPHeader } from './header.js'
import { Paragraph } from './paragraph.js'
Expand All @@ -26,9 +27,53 @@ import type { ParseOptions } from '../types/parser.js'
export class Section {
constructor(public paragraphs: Paragraph[]) {}

static fromEntry(entry: CFB$Entry, header: HWPHeader, options: ParseOptions): Section {
const content = Uint8Array.from(entry.content)
static fromCFB(
container: CFB$Container,
index: number,
header: HWPHeader,
options: ParseOptions,
) {
if (header.flags.distributed) {
return Section.fromDistributed(container, index, header, options)
}
return Section.fromNormal(container, index, header, options)
}

static fromNormal(
container: CFB$Container,
index: number,
header: HWPHeader,
options: ParseOptions,
) {
const entry = find(container, `Root Entry/BodyText/Section${index}`)

if (!entry) {
throw new Error('Section not exist')
}

return Section.fromBytes(Uint8Array.from(entry.content), header, options)
}

static fromDistributed(
container: CFB$Container,
index: number,
header: HWPHeader,
options: ParseOptions,
) {
const entry = find(container, `Root Entry/ViewText/Section${index}`)
if (!entry) {
throw new Error('Section not exist')
}

const content = decodeContent(entry)
return Section.fromBytes(content, header, options)
}

static fromBytes(
content: Uint8Array,
header: HWPHeader,
options: ParseOptions,
): Section {
if (header.flags.compressed) {
const decoded = inflate(content, { windowBits: -15 })
return Section.fromBuffer(decoded.buffer, header, options)
Expand All @@ -37,7 +82,11 @@ export class Section {
return Section.fromBuffer(content.buffer, header, options)
}

static fromBuffer(buffer: ArrayBuffer, header: HWPHeader, options: ParseOptions): Section {
static fromBuffer(
buffer: ArrayBuffer,
header: HWPHeader,
options: ParseOptions,
): Section {
const reader = new ByteReader(buffer)
const records = new PeekableIterator(reader.records())
const paragraphs: Paragraph[] = []
Expand All @@ -47,3 +96,45 @@ export class Section {
return new Section(paragraphs)
}
}

function createRand(seed = 1) {
let randomSeed = seed
return () => {
randomSeed = (randomSeed * 214013 + 2531011) & 0xffffffff
return (randomSeed >> 16) & 0x7fff
}
}

function decrypt(cipherText: ArrayBuffer, decKey: ArrayBuffer) {
// eslint-disable-next-line new-cap
const aesEcb = new aesjs.ModeOfOperation.ecb(new Uint8Array(decKey))
const decryptedBytes = aesEcb.decrypt(new Uint8Array(cipherText))
return decryptedBytes
}

function getDecryptionKey(data: ArrayBuffer): ArrayBuffer {
const sha1Encoded = new Uint8Array(data)
const sha1Decoded = new Uint8Array(sha1Encoded.length)
const seed = new DataView(data.slice(0, 4)).getInt32(0, true)
const offset = 4 + (seed & 0xf)
const rand = createRand(seed)
for (let j = 0, n = 0, k = 0; j < 256; j += 1, n -= 1) {
if (n === 0) {
k = rand() & 0xff
n = (rand() & 0xf) + 1
}
sha1Decoded[j] = sha1Encoded[j] ^ k
}
const sha1ucsstr = sha1Decoded.slice(offset, 80)
return sha1ucsstr.slice(0, 16)
}

function decodeContent(entry: CFB$Entry) {
const content = new Uint8Array(entry.content)
const reader = new ByteReader(content.buffer)
const { data } = reader.readRecord()
const encryptedData = reader.read(reader.remainByte())
const decKey = getDecryptionKey(data)
const decrypted = decrypt(encryptedData, decKey)
return decrypted
}
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d4ac55b

Please sign in to comment.