neoclide/coc.nvim · error

Invalid extraction directory: ${current}

Error message

Invalid extraction directory: ${current}

What it means

ensureNoSymlink walks each intermediate directory component of the extraction path and lstats it. If a component exists but is a regular file (or anything not a directory), the archive cannot contain children beneath it, so extraction aborts with 'Invalid extraction directory: <path>'. This prevents archives from overwriting or tunneling through non-directory entries.

Source

Thrown at src/model/download.ts:150

          let input = await openZipEntry(zipfile, entry)
          await writeZipEntry(input, target)
        }
        zipfile.readEntry()
      }, fail).catch(fail)
    })
    zipfile.readEntry()
  })
}

async function ensureNoSymlink(dest: string, target: string): Promise<void> {
  let relative = path.relative(dest, target)
  let current = dest
  for (let part of relative.split(path.sep).filter(Boolean)) {
    current = path.join(current, part)
    try {
      let stat = await fs.promises.lstat(current)
      if (stat.isSymbolicLink()) throw new Error(`Refusing to extract through symbolic link: ${current}`)
      if (!stat.isDirectory()) throw new Error(`Invalid extraction directory: ${current}`)
    } catch (e) {
      if ((e as NodeJS.ErrnoException).code !== 'ENOENT') throw e
    }
  }
}

async function writeZipEntry(input: NodeJS.ReadableStream, target: string): Promise<void> {
  let flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC
  if (typeof fs.constants.O_NOFOLLOW === 'number') flags |= fs.constants.O_NOFOLLOW
  let handle = await fs.promises.open(target, flags, 0o666)
  try {
    await pipeline(input, handle.createWriteStream())
  } finally {
    await handle.close().catch(() => undefined)
  }
}

/**

View on GitHub (pinned to 50e974d969)

Solutions

  1. Remove or rename the conflicting non-directory file at the reported path, then retry the extraction.
  2. Use a clean, empty destination directory for the extraction.
  3. Compare archive entry names against existing dest contents and resolve collisions before extracting.

Example fix

// before
// dest contains file 'lib', archive contains 'lib/foo.js'
download({ url, dest: '/opt/app', extract: true })
// after
fs.rmSync('/opt/app/lib'); // or rename it
download({ url, dest: '/opt/app', extract: true })
Defensive patterns

Strategy: validation

Validate before calling

const fs = require('fs'), path = require('path')
function destIsCleanExtractionRoot(dest) {
  if (!fs.existsSync(dest)) return true
  return fs.statSync(dest).isDirectory()
  // ideally also list archive entry dirs and ensure none collide with files in dest
}

Try / catch

try {
  await download({ url, dest, extract: true })
} catch (e) {
  if (String(e.message).startsWith('Invalid extraction directory:')) {
    const bad = e.message.split(': ')[1]
    fs.rmSync(bad, { force: true }) // after confirming it is safe to remove
    return retryExtraction()
  }
  throw e
}

Prevention

When it happens

Trigger: Extracting an archive whose entry path passes through a component that already exists on disk as a plain file, e.g. dest contains a file 'build' and the archive has entry 'build/output.js'.

Common situations: Re-extracting into a directory previously partially populated; a leftover file from an earlier failed extraction; name collisions where an archive directory name matches an existing file.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/7dab52e9d5680743. Report an issue: GitHub.