Files
ArtPlayer/scripts/documentation/files.ts

59 lines
1.6 KiB
TypeScript

import assert from 'node:assert/strict'
import crypto from 'node:crypto'
import fs from 'node:fs'
import path from 'node:path'
export function sha256(text: string): string {
return crypto.createHash('sha256').update(text).digest('hex')
}
export function read(file: string): string {
return fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
}
export function ownedPath(root: string, relative: string): string {
assert(
relative
&& !relative.includes('\\')
&& !path.isAbsolute(relative)
&& !relative.split('/').includes('..'),
'Invalid documentation path',
)
const base = fs.realpathSync(root)
const file = path.resolve(base, relative)
let ancestor = file
while (true) {
try {
fs.lstatSync(ancestor)
break
}
catch (error) {
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
throw error
const parent = path.dirname(ancestor)
assert(parent !== ancestor, 'No existing documentation path ancestor')
ancestor = parent
}
}
const inside = path.relative(base, fs.realpathSync(ancestor))
assert(
inside !== '..'
&& !inside.startsWith(`..${path.sep}`)
&& !path.isAbsolute(inside),
'Documentation path escapes workspace',
)
return file
}
export function atomicWrite(file: string, content: string): void {
fs.mkdirSync(path.dirname(file), { recursive: true })
const temporary = `${file}.${crypto.randomUUID()}.tmp`
try {
fs.writeFileSync(temporary, content, { flag: 'wx' })
fs.renameSync(temporary, file)
}
finally {
if (fs.existsSync(temporary))
fs.unlinkSync(temporary)
}
}