Skip to content
Merged
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
13 changes: 0 additions & 13 deletions .eslintrc

This file was deleted.

4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ jobs:

strategy:
matrix:
node-version: [16, 18, 20, 22]
node-version: [20, 22, 24]
os: [ubuntu-latest, windows-latest, macOS-latest]

steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm install
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
.idea/
node_modules/
bin/
.nyc_output/
2 changes: 1 addition & 1 deletion .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
bin/
.nyc_output/
fixtures/
1 change: 0 additions & 1 deletion .taprc

This file was deleted.

18 changes: 18 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const js = require('@eslint/js')
const globals = require('globals')
const prettierRecommended = require('eslint-plugin-prettier/recommended')

module.exports = [
js.configs.recommended,
prettierRecommended,
{
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'commonjs',
globals: globals.node,
},
rules: {
'prefer-const': 'error',
},
},
]
Binary file added fixtures/du.zip
Binary file not shown.
Binary file added fixtures/zip-slip.zip
Binary file not shown.
38 changes: 35 additions & 3 deletions get-sysinternals-du.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,43 @@ const fs = require('fs')
const os = require('os')
const https = require('https')
const path = require('path')
const decompress = require('decompress')
const { execFileSync } = require('child_process')
const { HttpsProxyAgent } = require('https-proxy-agent')

// the only entries of DU.zip we need; nothing else is ever written into bin/
const DU_ZIP_ENTRIES = ['du.exe', 'du64.exe', 'du64a.exe', 'Eula.txt']

// the bsdtar shipped with Windows 10+ reads zip archives. use it by full path
// so that a GNU tar found earlier on PATH (e.g. Git for Windows) isn't picked
// up instead
function windowsTar() {
return path.join(
process.env.SystemRoot || 'C:\\Windows',
'System32',
'tar.exe',
)
}

// tar is only ever asked to list the archive and to stream single entries to
// stdout, never to write to disk: the files are written here, by name, into
// destDir. entry paths, `..` components and links inside the archive can
// therefore never decide where anything is written
exports.extractDuZip = function (zipPath, destDir, tar = windowsTar()) {
const run = args => execFileSync(tar, args, { maxBuffer: 64 * 1024 * 1024 })

const entries = run(['-tf', zipPath]).toString().split(/\r?\n/)

fs.mkdirSync(destDir, { recursive: true })

for (const entry of DU_ZIP_ENTRIES) {
if (entries.includes(entry)) {
fs.writeFileSync(path.join(destDir, entry), run(['-xOf', zipPath, entry]))
}
}
}

exports.onDuZipDownloaded = function (tempFilePath, workspace) {
decompress(tempFilePath, path.join(workspace, 'bin'))
exports.extractDuZip(tempFilePath, path.join(workspace, 'bin'))
}

exports.downloadDuZip = function (mirror, workspace) {
Expand All @@ -27,7 +59,7 @@ exports.downloadDuZip = function (mirror, workspace) {
console.log(`downloading du.zip from ${duZipLocation}`)
if (!mirror) {
console.log(
`if you have trouble while downloading, try set process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION to a proper mirror or local file path`
`if you have trouble while downloading, try set process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION to a proper mirror or local file path`,
)
}

Expand Down
202 changes: 128 additions & 74 deletions get-sysinternals-du.test.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,142 @@
if (process.platform !== 'win32') {
// these test cases are only for win32
return
}

const { test, beforeEach } = require('tap')
const path = require('path')
const os = require('os')
const fs = require('fs')
const { test, beforeEach } = require('node:test')
const assert = require('node:assert/strict')
const path = require('node:path')
const os = require('node:os')
const fs = require('node:fs')
const subject = require('./get-sysinternals-du.js')

// these test cases are only for win32
const skip = process.platform !== 'win32'

const workspace = path.join(os.tmpdir(), 'fast-folder-size-playground')
beforeEach(() => {
if (fs.existsSync(workspace)) fs.rmSync(workspace, { recursive: true })
fs.mkdirSync(workspace)
})

test('it can use local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => {
t.test('C:\\**\\du.zip', t => {
const dummyDuZipPath = path.join(workspace, 'dummy-du.zip')
fs.writeFileSync(dummyDuZipPath, '')
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath

subject.onDuZipDownloaded = function (tempFilePath) {
t.equal(tempFilePath, dummyDuZipPath)
t.end()
}
test(
'it can use local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION',
{ skip },
async t => {
await t.test('C:\\**\\du.zip', () => {
const dummyDuZipPath = path.join(workspace, 'dummy-du.zip')
fs.writeFileSync(dummyDuZipPath, '')
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath

let received
subject.onDuZipDownloaded = tempFilePath => (received = tempFilePath)

subject.default(workspace)
assert.equal(received, dummyDuZipPath)
})

await t.test('C://**/du.zip', () => {
const dummyDuZipPath = path
.join(workspace, 'dummy-du.zip')
.replaceAll('\\', '/')
.replace(':/', '://')
fs.writeFileSync(dummyDuZipPath, '')
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath

let received
subject.onDuZipDownloaded = tempFilePath => (received = tempFilePath)

subject.default(workspace)
assert.equal(received, dummyDuZipPath)
})
},
)

test(
'it cannot use non-exists local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION',
{ skip },
() => {
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = path.join(
workspace,
'non-exists-dummy-du.zip',
)

assert.throws(() => subject.default(workspace), {
message: /^du\.zip not found at/,
})
},
)

test(
'it can use http(s) url as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION',
{ skip },
() => {
const dummyUrl = 'https://non-exists.localhost/du.zip'
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyUrl

let received
subject.downloadDuZip = mirror => (received = mirror)

subject.default(workspace)
})

t.test('C://**/du.zip', t => {
const dummyDuZipPath = path
.join(workspace, 'dummy-du.zip')
.replaceAll('\\', '/')
.replace(':/', '://')
fs.writeFileSync(dummyDuZipPath, '')
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyDuZipPath

subject.onDuZipDownloaded = function (tempFilePath) {
t.equal(tempFilePath, dummyDuZipPath)
t.end()
assert.equal(received, dummyUrl)
},
)

test(
'when process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION not found, then download it directly',
{ skip },
() => {
delete process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION

let called = false
let received
subject.downloadDuZip = mirror => {
called = true
received = mirror
}

subject.default(workspace)
})

t.end()
})

test('it cannot use non-exists local file path as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => {
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = path.join(
workspace,
'non-exists-dummy-du.zip'
)

t.throws(
() => subject.default(workspace),
error => {
return error.message.startsWith('du.zip not found at')
assert.ok(called)
assert.equal(received, undefined)
},
)

test(
'extractDuZip copies only the known du.zip entries into bin',
{ skip },
() => {
const bin = path.join(workspace, 'bin')

subject.extractDuZip(path.join(__dirname, 'fixtures', 'du.zip'), bin)

assert.deepEqual(fs.readdirSync(bin).sort(), [
'Eula.txt',
'du.exe',
'du64.exe',
'du64a.exe',
])
assert.equal(
fs.readFileSync(path.join(bin, 'du64.exe'), 'utf8'),
'fake du64.exe',
)
},
)

test(
'extractDuZip never writes outside the destination (zip slip)',
{ skip },
() => {
const bin = path.join(workspace, 'bin')
const outside = [
// relative to the destination
path.join(workspace, 'escaped.txt'),
path.join(workspace, 'escaped-via-link.txt'),
path.join(path.dirname(workspace), 'escaped-deeper.txt'),
// relative to the current directory, in case anything extracts there
path.join(process.cwd(), 'link'),
path.join(path.dirname(process.cwd()), 'escaped.txt'),
]

subject.extractDuZip(path.join(__dirname, 'fixtures', 'zip-slip.zip'), bin)

assert.deepEqual(fs.readdirSync(bin), ['du.exe'])
for (const file of outside) {
assert.ok(!fs.existsSync(file), `${file} must not exist`)
}
)

t.end()
})

test('it can use http(s) url as process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION', t => {
const dummyUrl = 'https://non-exists.localhost/du.zip'
process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION = dummyUrl

subject.downloadDuZip = function (mirror) {
t.equal(mirror, dummyUrl)
t.end()
}

subject.default(workspace)
})

test('when process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION not found, then download it directly', t => {
delete process.env.FAST_FOLDER_SIZE_DU_ZIP_LOCATION

subject.downloadDuZip = function (mirror) {
t.equal(mirror, undefined)
t.end()
}

subject.default(workspace)
})
},
)
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function fastFolderSize(target, options, cb) {
const bytes = processFn(stdout)

cb(null, bytes)
}
},
)
}

Expand Down
2 changes: 1 addition & 1 deletion os.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const commands = {
win32: `"${path.join(
__dirname,
'bin',
`du${process.arch === 'x64' ? '64' : ''}.exe`
`du${process.arch === 'x64' ? '64' : ''}.exe`,
)}" -nobanner -accepteula -q -c .`,

// macos
Expand Down
Loading
Loading