From 607230c09ef32020bd493bb7b5b4c49e3ff63f0e Mon Sep 17 00:00:00 2001 From: undefined Date: Sat, 25 Nov 2023 03:53:29 +0800 Subject: [PATCH] core: install: strip zip prefix --- packages/hydrooj/src/commands/install.ts | 7 +-- packages/utils/lib/utils.ts | 56 +++++++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/hydrooj/src/commands/install.ts b/packages/hydrooj/src/commands/install.ts index 7f0a7618..8a7fc806 100644 --- a/packages/hydrooj/src/commands/install.ts +++ b/packages/hydrooj/src/commands/install.ts @@ -6,7 +6,7 @@ import { CAC } from 'cac'; import fs from 'fs-extra'; import superagent from 'superagent'; import tar from 'tar'; -import { Logger, streamToBuffer } from '@hydrooj/utils'; +import { extractZip, Logger } from '@hydrooj/utils'; const logger = new Logger('install'); let yarnVersion = 0; @@ -32,8 +32,8 @@ function downloadAndExtractTgz(url: string, dest: string) { }); } async function downloadAndExtractZip(url: string, dest: string) { - const res = await streamToBuffer(await superagent.get(url).responseType('blob')); - new AdmZip(res).extractAllTo(dest, true); + const res = await superagent.get(url).responseType('arraybuffer'); + await extractZip(new AdmZip(res.body), dest, true, true); } const types = { '.tgz': downloadAndExtractTgz, @@ -71,6 +71,7 @@ export function register(cli: CAC) { newAddonPath = path.join(addonDir, name); logger.info(`Downloading ${src} to ${newAddonPath}`); fs.ensureDirSync(newAddonPath); + fs.emptyDirSync(newAddonPath); const func = types[Object.keys(types).find((i) => filename.endsWith(i))]!; await func(src, newAddonPath); } else throw new Error('Unsupported file type'); diff --git a/packages/utils/lib/utils.ts b/packages/utils/lib/utils.ts index 1adedf14..0f889593 100644 --- a/packages/utils/lib/utils.ts +++ b/packages/utils/lib/utils.ts @@ -1,12 +1,14 @@ import crypto from 'crypto'; import os from 'os'; import path from 'path'; -import { Duplex } from 'stream'; +import { Duplex, PassThrough } from 'stream'; import { inspect } from 'util'; +import type AdmZip from 'adm-zip'; import fs from 'fs-extra'; import moment, { isMoment, Moment } from 'moment-timezone'; import { ObjectId } from 'mongodb'; import Logger from 'reggol'; +import type { Request } from 'superagent'; export * as yaml from 'js-yaml'; export * as fs from 'fs-extra'; @@ -80,7 +82,16 @@ export function isClass(obj: any, strict = false): obj is new (...args: any) => return false; } -export function streamToBuffer(stream: any, maxSize = 0): Promise { +function isSuperagentRequest(t: NodeJS.ReadableStream | Request): t is Request { + return 'req' in t; +} +export function streamToBuffer(input: NodeJS.ReadableStream | Request, maxSize = 0): Promise { + let stream: NodeJS.ReadableStream; + if (isSuperagentRequest(input)) { + const s = new PassThrough(); + input.pipe(s); + stream = s; + } else stream = input; return new Promise((resolve, reject) => { const buffers = []; let length = 0; @@ -271,4 +282,45 @@ export function Counter() { }) as Record; } +function canonical(p: string) { + if (!p) return ''; + const safeSuffix = path.posix.normalize(`/${p.split('\\').join('/')}`); + return path.join('.', safeSuffix); +} + +function sanitize(prefix: string, name: string) { + prefix = path.resolve(path.normalize(prefix)); + const parts = name.split('/'); + for (let i = 0, l = parts.length; i < l; i++) { + const p = path.normalize(path.join(prefix, parts.slice(i, l).join(path.sep))); + if (p.indexOf(prefix) === 0) { + return p; + } + } + return path.normalize(path.join(prefix, path.basename(name))); +} + +export function sanitizePath(pathname: string) { + const parts = pathname.replace(/\\/g, '/').split('/').filter((i) => i && i !== '.' && i !== '..'); + return parts.join(path.sep); +} + +/* eslint-disable no-await-in-loop */ +export async function extractZip(zip: AdmZip, dest: string, overwrite = false, strip = false) { + const entries = zip.getEntries(); + const shouldStrip = strip ? entries.every((i) => i.entryName.startsWith(entries[0].entryName)) : false; + for (const entry of entries) { + const name = shouldStrip ? entry.entryName.substring(entries[0].entryName.length) : entry.entryName; + const d = sanitize(dest, canonical(name)); + if (entry.isDirectory) { + await fs.mkdir(d, { recursive: true }); + continue; + } + const content = entry.getData(); + if (!content) throw new Error('CANT_EXTRACT_FILE'); + if (!fs.existsSync(d) || overwrite) await fs.writeFile(d, content); + await fs.utimes(d, entry.header.time, entry.header.time); + } +} + export * from '@hydrooj/utils/lib/common';