Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 134 additions & 134 deletions dist/index.js

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions src/install-pnpm/locked-version.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { syncBuiltinESMExports } from 'node:module'
import { afterEach, test } from 'node:test'
import { readLockedPnpmVersion } from './locked-version.ts'

Expand Down Expand Up @@ -85,3 +86,89 @@ test('reads a lockfile that holds only the package-manager document', () => {
fs.writeFileSync(lockfile, fs.readFileSync(lockfile, 'utf8').replace(/\n---\n[\s\S]*$/, '\n'))
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1')
})

test('recognizes separators at every position across a read boundary', () => {
const directory = fixture()
const lockfile = path.join(directory, 'pnpm-lock.yaml')
const env = fs.readFileSync(lockfile, 'utf8').split('\n---\n')[0]
for (const newline of ['\n', '\r\n']) {
for (const bom of ['', '\uFEFF']) {
const prefix = bom + env.replace(/\n/g, newline) + newline + '# '
for (let offset = -7; offset <= 1; offset++) {
const padding = 64 * 1024 + offset - Buffer.byteLength(prefix)
fs.writeFileSync(lockfile, prefix + 'x'.repeat(padding) + newline + '---' + newline + 'packages: [')
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1', `${JSON.stringify({ newline, bom, offset })}`)
}
}
}
})

test('preserves UTF-8 split across reads and env-only closing separators', () => {
const directory = fixture()
const lockfile = path.join(directory, 'pnpm-lock.yaml')
const env = fs.readFileSync(lockfile, 'utf8').split('\n---\n')[0]
for (const ending of ['', '\n', '\n---', '\n---\n']) {
const prefix = '---\n# '
fs.writeFileSync(lockfile, prefix + 'x'.repeat(65535 - prefix.length) + 'é🦀\n' + env.slice(4) + ending)
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1')
}
const prefix = '---\n# '
const anchor = '\npin: &'
fs.writeFileSync(lockfile, prefix + 'x'.repeat(65535 - prefix.length - anchor.length) +
anchor + 'é 12.8.1\n' + env.slice(4).replace('version: 12.8.1', 'version: *é') + '\n---\n')
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1')
fs.writeFileSync(lockfile, env + '\n# ' + 'x'.repeat(65536) + '\ninvalid: [')
assert.throws(() => readLockedPnpmVersion(directory, '12'))
})

test('handles short reads without changing document-start or boundary semantics', t => {
const originalRead = fs.readSync
let limit = 1
t.mock.method(fs, 'readSync', (fd, buffer, offset, length, position) =>
originalRead(fd, buffer, offset, Math.min(length, limit), position))
syncBuiltinESMExports()
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports() })
const directory = fixture()
const lockfile = path.join(directory, 'pnpm-lock.yaml')
const valid = fs.readFileSync(lockfile, 'utf8')
for (limit = 1; limit <= 8; limit++) {
for (const content of [valid, '\uFEFF' + valid.replace(/\n/g, '\r\n')]) {
fs.writeFileSync(lockfile, content)
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1')
}
for (const content of ['', '---', '\uFEFF\uFEFF' + valid, '--- # comment\n' + valid, '---\n---\n']) {
fs.writeFileSync(lockfile, content)
assert.equal(readLockedPnpmVersion(directory, '12'), undefined)
}
}
})

test('bounds reads for dependency documents and closes the file after read errors', t => {
const originalRead = fs.readSync, originalClose = fs.closeSync
let bytes = 0, closed = 0, failure
t.mock.method(fs, 'readSync', (...args) => {
if (failure) throw failure
const count = originalRead(...args)
bytes += count
return count
})
t.mock.method(fs, 'closeSync', fd => { closed++; return originalClose(fd) })
syncBuiltinESMExports()
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports() })
const directory = fixture()
const lockfile = path.join(directory, 'pnpm-lock.yaml')
fs.appendFileSync(lockfile, '# ' + 'x'.repeat(1024 * 1024))
closed = 0
assert.equal(readLockedPnpmVersion(directory, '12'), '12.8.1')
assert.equal(bytes, 64 * 1024)
assert.equal(closed, 1)
fs.writeFileSync(lockfile, '# ' + 'x'.repeat(1024 * 1024))
bytes = 0
closed = 0
assert.equal(readLockedPnpmVersion(directory, '12'), undefined)
assert.equal(bytes, 64 * 1024)
assert.equal(closed, 1)
failure = Object.assign(new Error('read failed'), { code: 'EIO' })
assert.throws(() => readLockedPnpmVersion(directory, '12'), error => error === failure)
assert.equal(closed, 2)
})
40 changes: 36 additions & 4 deletions src/install-pnpm/locked-version.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from 'fs'
import { closeSync, openSync, readSync } from 'fs'
import path from 'path'
import semver from 'semver'
import { StringDecoder } from 'string_decoder'
import { parseDocument } from 'yaml'

/**
Expand All @@ -11,15 +12,14 @@ export function readLockedPnpmVersion(directory: string, spec: string): string |
// Exact pins already identify the binary; tags need registry resolution.
if (semver.valid(spec) || !semver.validRange(spec)) return undefined

let content: string
let envDocument: string | undefined
try {
content = readFileSync(path.join(directory, 'pnpm-lock.yaml'), 'utf8')
envDocument = readEnvDocument(path.join(directory, 'pnpm-lock.yaml'))
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
throw error
}

const envDocument = extractEnvDocument(content)
if (envDocument === undefined) return undefined
const document = parseDocument(envDocument)
if (document.errors.length) throw document.errors[0]
Expand All @@ -33,6 +33,38 @@ export function readLockedPnpmVersion(directory: string, spec: string): string |
return undefined
}

function readEnvDocument(filename: string): string | undefined {
const fd = openSync(filename, 'r')
try {
const buffer = Buffer.allocUnsafe(64 * 1024)
const decoder = new StringDecoder('utf8')
const chunks: string[] = []
const separator = /\r?\n---\r?\n/g
let tail = ''
let checkedStart = false
while (true) {
const bytesRead = readSync(fd, buffer, 0, buffer.length, null)
if (bytesRead === 0) return extractEnvDocument(chunks.join('') + decoder.end())
const chunk = decoder.write(buffer.subarray(0, bytesRead))
chunks.push(chunk)
const content = tail + chunk
if (!checkedStart && content.length < 6) {
tail = content
continue
}
const start = checkedStart ? 0 : /^\uFEFF?---\r?\n/.exec(content)?.[0].length
if (start === undefined) return undefined
separator.lastIndex = start
if (separator.test(content)) return extractEnvDocument(chunks.join(''))
checkedStart = true
// Retain enough text for a CRLF separator split across reads.
tail = content.slice(Math.max(start, content.length - 6))
}
} finally {
closeSync(fd)
}
}

/**
* pnpm 11+ records package-manager dependencies in a document that leads the
* lockfile. Slicing it out the way pnpm's `extractEnvDocument` does avoids
Expand Down
Loading