Skip to content

parseConfigGypi fails on GYP files whose single-quoted strings contain double-quote characters (e.g. canvas, 'OS=="win"' conditions) #3333

@vbjay

Description

@vbjay

Description

parseConfigGypi in lib/create-config-gypi.js converts single-quoted GYP strings to JSON by doing a bare global substitution:

config = config.replace(/'/g, '"')

This breaks whenever a single-quoted string itself contains double-quote characters, which is ubiquitous in GYP condition strings like:

['OS=="win"', { … }]

After the substitution that becomes:

["OS==""win"", { … }]

which is invalid JSON and causes JSON.parse to throw:

SyntaxError: Expected ',' or ']' after array element in JSON at position 31 (line 3 column 12)

Affected versions

Reproduced on v12.3.0 and v13.0.0 (latest). The three-step implementation of parseConfigGypi has been unchanged since the function was introduced.

Reproduction

const { parseConfigGypi } = require('node-gyp/lib/create-config-gypi')
// Any real-world binding.gyp that uses condition strings — e.g. canvas@2.11.2
const raw = require('fs').readFileSync('/path/to/canvas/binding.gyp', 'utf8')
parseConfigGypi(raw) // → SyntaxError

canvas@2.11.2 is a good, publicly available repro: install it with --ignore-scripts, then call parseConfigGypi on its binding.gyp.

Root cause

Step 3 of parseConfigGypi is a naive global '" replacement. It needs to be a proper tokenizer that converts single-quoted strings to double-quoted JSON strings while escaping any double-quote characters found inside those strings.

Suggested fix

Replace the global regex with a character-by-character pass:

function parseConfigGypi(raw) {
  // 1. Strip # comments
  let src = raw.replace(/#[^\n]*/g, '')
  // 2. Join Python-style multiline string continuations
  src = src.replace(/'\\?\n\s*'/g, '')

  // 3. Tokenise: convert single-quoted strings to double-quoted JSON strings,
  //    escaping any embedded double-quote characters.
  let out = ''
  let i = 0
  while (i < src.length) {
    const ch = src[i]
    if (ch === "'") {
      let s = '"'
      i++
      while (i < src.length && src[i] !== "'") {
        if (src[i] === '"') {
          s += '\\"'
        } else if (src[i] === '\\') {
          s += src[i] + (src[i + 1] || '')
          i++
        } else {
          s += src[i]
        }
        i++
      }
      s += '"'
      out += s
      i++ // skip closing `'`
    } else if (ch === '"') {
      // Copy already-valid double-quoted strings verbatim
      out += ch
      i++
      while (i < src.length && src[i] !== '"') {
        if (src[i] === '\\') { out += src[i] + (src[i + 1] || ''); i += 2 }
        else { out += src[i]; i++ }
      }
      out += src[i] || ''
      i++
    } else {
      out += ch
      i++
    }
  }

  // 4. Strip trailing commas (also invalid in JSON, common in GYP)
  out = out.replace(/,(\s*[}\]])/g, '$1')
  return JSON.parse(out)
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions