update and test

This commit is contained in:
Simon Knott 2024-12-12 10:42:43 +01:00
parent 33cdb9c8e7
commit a526cb9c81
No known key found for this signature in database
GPG key ID: 8CEDC00028084AEC
4 changed files with 98 additions and 191 deletions

View file

@ -14,13 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
import fs from 'fs'; import fs from 'fs';
import zlib from 'zlib'; import { Writable } from 'stream';
import { Transform } from 'stream';
import path from 'path'; import path from 'path';
import { execSync } from 'child_process';
class TarHeader { function parseHeader(buffer: Buffer) {
static parseHeader(buffer) {
if (buffer.length < 512) if (buffer.length < 512)
return null; return null;
@ -56,23 +53,21 @@ class TarHeader {
uid, uid,
gid gid
}; };
}
} }
class TarTransformer extends Transform { type TarHeader = NonNullable<ReturnType<typeof parseHeader>>;
constructor(outputPath, prefix = 'chrome-mac') {
export class TarExtractor extends Writable {
private buffer = Buffer.alloc(0);
private currentHeader: TarHeader | null = null;
private remainingBytes = 0;
private currentFileStream: fs.WriteStream | null = null;
constructor(private outputPath: (path: string) => string) {
super(); super();
this.outputPath = outputPath;
this.prefix = prefix;
this.buffer = Buffer.alloc(0);
this.currentHeader = null;
this.remainingBytes = 0;
this.symlinksToCreate = new Map();
this.currentFileStream = null;
this.filesToChmod = new Set();
} }
async mkdir(dir) { async mkdir(dir: string) {
try { try {
await fs.promises.mkdir(dir, { recursive: true }); await fs.promises.mkdir(dir, { recursive: true });
// Set proper permissions for directories // Set proper permissions for directories
@ -83,16 +78,8 @@ class TarTransformer extends Transform {
} }
} }
normalizePath(headerName) { async processHeader(header: TarHeader) {
if (!headerName.startsWith(this.prefix)) const fullPath = this.outputPath(header.name);
return path.join(this.prefix, headerName);
return headerName;
}
async processHeader(header) {
const normalizedPath = this.normalizePath(header.name);
const fullPath = path.join(this.outputPath, normalizedPath);
await this.mkdir(path.dirname(fullPath)); await this.mkdir(path.dirname(fullPath));
if (header.type === 'directory') { if (header.type === 'directory') {
@ -101,21 +88,16 @@ class TarTransformer extends Transform {
} }
if (header.type === 'symlink') { if (header.type === 'symlink') {
this.symlinksToCreate.set(fullPath, header.linkname); await this.createSymlink(fullPath, header.linkname);
return null; return null;
} }
// Track files that need chmod // TODO: track chmod maybe
this.filesToChmod.add({ path: fullPath, mode: header.mode });
return fs.createWriteStream(fullPath, { mode: header.mode }); return fs.createWriteStream(fullPath, { mode: header.mode });
} }
async createSymlinks() { async createSymlink(symlinkPath: string, targetPath: string) {
for (const [symlinkPath, targetPath] of this.symlinksToCreate) {
try {
const linkDir = path.dirname(symlinkPath);
try { try {
await fs.promises.unlink(symlinkPath); await fs.promises.unlink(symlinkPath);
} catch (err) { } catch (err) {
@ -124,95 +106,27 @@ class TarTransformer extends Transform {
} }
await fs.promises.symlink(targetPath, symlinkPath); await fs.promises.symlink(targetPath, symlinkPath);
} catch (err) {
console.error(`Failed to create symlink ${symlinkPath} -> ${targetPath}:`, err);
}
}
} }
async fixMacOSApp() { override async _write(chunk: Buffer, _encoding: string, callback: (err?: Error) => void) {
try {
const appPath = path.join(this.outputPath, this.prefix, 'Chromium.app');
// Remove quarantine attribute
execSync(`xattr -r -d com.apple.quarantine "${appPath}"`, { stdio: 'ignore' });
// Set proper permissions recursively
execSync(`chmod -R u+x "${appPath}"`, { stdio: 'ignore' });
// Mark as executable
execSync(`chmod +x "${appPath}/Contents/MacOS/Chromium"`, { stdio: 'ignore' });
// Fix permissions for all binary files
for (const file of this.filesToChmod) {
if ((file.mode & 0o111) !== 0) { // If file has any execute bits
await fs.promises.chmod(file.path, file.mode);
}
}
} catch (err) {
console.error('Error fixing macOS app:', err);
}
}
async processHeader(header) {
const normalizedPath = this.normalizePath(header.name);
const fullPath = path.join(this.outputPath, normalizedPath);
await this.mkdir(path.dirname(fullPath));
if (header.type === 'directory') {
await this.mkdir(fullPath);
return null;
}
if (header.type === 'symlink') {
this.symlinksToCreate.set(fullPath, header.linkname);
return null;
}
return fs.createWriteStream(fullPath, { mode: header.mode });
}
async createSymlinks() {
for (const [symlinkPath, targetPath] of this.symlinksToCreate) {
try {
const linkDir = path.dirname(symlinkPath);
const resolvedTarget = path.resolve(linkDir, targetPath);
try {
await fs.promises.unlink(symlinkPath);
} catch (err) {
if (err.code !== 'ENOENT')
throw err;
}
await fs.promises.symlink(targetPath, symlinkPath);
} catch (err) {
console.error(`Failed to create symlink ${symlinkPath} -> ${targetPath}:`, err);
}
}
}
// ... rest of the implementation (same _transform and other methods) ...
async _transform(chunk, encoding, callback) {
try { try {
this.buffer = Buffer.concat([this.buffer, chunk]); this.buffer = Buffer.concat([this.buffer, chunk]);
while (this.buffer.length >= 512) { while (this.buffer.length >= 512) {
if (!this.currentHeader) { if (!this.currentHeader) {
// Check for end of archive (two consecutive zero blocks) // Check for end of archive (two consecutive zero blocks)
if (this.buffer.slice(0, 512).every(byte => byte === 0)) { if (this.buffer.subarray(0, 512).every(byte => byte === 0)) {
this.buffer = this.buffer.slice(512); this.buffer = this.buffer.subarray(512);
continue; continue;
} }
const header = TarHeader.parseHeader(this.buffer); const header = parseHeader(this.buffer);
if (!header) if (!header)
break; break;
this.currentHeader = header; this.currentHeader = header;
this.remainingBytes = header.size; this.remainingBytes = header.size;
this.buffer = this.buffer.slice(512); this.buffer = this.buffer.subarray(512);
if (header.size > 0) if (header.size > 0)
this.currentFileStream = await this.processHeader(header); this.currentFileStream = await this.processHeader(header);
@ -229,30 +143,22 @@ class TarTransformer extends Transform {
continue; continue;
} }
const dataChunk = this.buffer.slice(0, blockSize); const dataChunk = this.buffer.subarray(0, blockSize);
this.buffer = this.buffer.slice(blockSize); this.buffer = this.buffer.subarray(blockSize);
this.remainingBytes -= blockSize; this.remainingBytes -= blockSize;
if (this.currentFileStream) { if (this.currentFileStream)
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => this.currentFileStream!.write(dataChunk, err => err ? reject(err) : resolve()));
this.currentFileStream.write(dataChunk, err => {
if (err)
reject(err);
else
resolve();
});
});
}
// Handle padding // Handle padding
if (this.remainingBytes === 0) { if (this.remainingBytes === 0) {
const padding = 512 - (this.currentHeader.size % 512); const padding = 512 - (this.currentHeader.size % 512);
if (padding < 512) if (padding < 512)
this.buffer = this.buffer.slice(padding); this.buffer = this.buffer.subarray(padding);
this.currentHeader = null; this.currentHeader = null;
if (this.currentFileStream) { if (this.currentFileStream) {
await new Promise(resolve => this.currentFileStream.end(resolve)); await new Promise(resolve => this.currentFileStream!.end(resolve));
this.currentFileStream = null; this.currentFileStream = null;
} }
} }
@ -262,38 +168,4 @@ class TarTransformer extends Transform {
callback(err); callback(err);
} }
} }
async _flush(callback) {
try {
if (this.currentFileStream)
await new Promise(resolve => this.currentFileStream.end(resolve));
await this.createSymlinks();
await this.fixMacOSApp();
callback();
} catch (err) {
callback(err);
}
}
} }
const inputPath = '/Users/maxschmitt/Downloads/chromium-mac.tar.br';
const outputPath = '/Users/maxschmitt/Downloads';
// Clean up previous extraction
fs.rmSync(path.join(outputPath, 'chrome-mac'), { recursive: true, force: true });
const input = fs.createReadStream(inputPath);
const brotli = zlib.createBrotliDecompress();
const tarTransformer = new TarTransformer(outputPath);
// Add error handlers
input.on('error', err => console.error('Input stream error:', err));
brotli.on('error', err => console.error('Brotli decompression error:', err));
tarTransformer.on('error', err => console.error('Tar extraction error:', err));
// Pipe everything together
input
.pipe(brotli)
.pipe(tarTransformer)
.on('finish', () => console.log('Extraction complete!'));

View file

@ -1 +1 @@
/Users/skn0tt/dev/microsoft/playwright/tar-example/foo ./foo

Binary file not shown.

View file

@ -0,0 +1,35 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { expect, test } from '@playwright/test';
import { createReadStream, constants } from 'fs';
import { access, readFile, readlink } from 'fs/promises';
import { TarExtractor } from '../../../packages/playwright-core/src/utils/tar';
import { finished } from 'stream/promises';
test('tar extractor', async () => {
const input = createReadStream('tests/assets/archive.tar');
const extractor = new TarExtractor(name => test.info().outputPath(name));
await finished(input.pipe(extractor));
expect(await readFile(test.info().outputPath('tar-example/foo'), { encoding: 'utf-8' })).toEqual('foo-content');
expect(await readFile(test.info().outputPath('tar-example/bar/baz'), { encoding: 'utf-8' })).toEqual('baz-content');
expect(await readFile(test.info().outputPath('tar-example/build.sh'), { encoding: 'utf-8' })).toEqual('echo "hello world"');
await access(test.info().outputPath('tar-example/build.sh'), constants.X_OK);
expect(await readlink(test.info().outputPath('tar-example/symlink'))).toEqual('./foo');
expect(await readFile(test.info().outputPath('tar-example/symlink'), { encoding: 'utf-8' })).toEqual('foo-content');
});