Skip to content

Escape license text embedded in the generated code - #47

Draft
yutailang0119 wants to merge 1 commit into
maiyama18:mainfrom
yutailang0119:fix/escape-generated-license-text
Draft

Escape license text embedded in the generated code#47
yutailang0119 wants to merge 1 commit into
maiyama18:mainfrom
yutailang0119:fix/escape-generated-license-text

Conversation

@yutailang0119

@yutailang0119 yutailang0119 commented Aug 18, 2026

Copy link
Copy Markdown

Problem

Plugins/LicensesPlugin/LicensesPlugin.swift embeds each dependency's LICENSE body into a plain Swift multiline string literal:

licenseText: """
\(licenseText)
"""

licenseText is never escaped, so a dependency whose LICENSE contains any of the following makes the generated Licenses+Generated.swift fail to compile — or, worse, silently alters the license text:

in the LICENSE result in the generated file
\ + an invalid escape character (e.g. \q) error: invalid escape sequence in literal
\n, \t, \0, … compiles, but the text is corrupted — becomes a real newline/tab
\(…) treated as string interpolation
""" error: multi-line string literal closing delimiter must begin on a new line

Backslashes are not hypothetical in license files: Windows paths, TeX/roff fragments, and quoted shell examples all appear in real LICENSE files, and BSD-style licenses sometimes embed quoted blocks.

What makes this particularly awkward is where the error lands. The generated file is compiled as part of the consuming target, so the user sees errors pointing into

.build/plugins/outputs/<pkg>/<target>/destination/LicensesPlugin/Output/Licenses+Generated.swift:3187:11: error: invalid escape sequence in literal

Nothing in that message names this plugin, and the file is inside .build, so it looks like a corrupted build directory rather than a code-generation bug.

Fix

Emit a raw string literal, and size the # delimiter to the content:

let delimiter = String(repeating: "#", count: rawDelimiterHashCount(for: licenseText))

A single # is not sufficient on its own, which is why the count is computed rather than hardcoded. A raw literal delimited by one #:

  • is still terminated by """#, and
  • still interprets \# as an escape introducer — so \#n in a LICENSE would still become a real newline.

rawDelimiterHashCount(for:) finds the longest run of # that follows either """ or \ anywhere in the text and returns one more than that (minimum 1). Every real license in PluginTests/ExamplePackage needs exactly one #; only the new fixture forces two.

The shape of the generated file is otherwise unchanged — the closing delimiter still sits at column 0, so multiline-literal indentation stripping still does not apply, and license bodies remain readable in the generated output.

Alternative I considered

Emitting an escaped single-line literal would sidestep delimiter sizing entirely and be trivially correct, but it collapses each license onto one very long line (Apache-2.0 is ~11 KB), which makes the generated file unpleasant to read when debugging. I kept the readable multiline output, but I am happy to switch if you would prefer the simpler generator.

Tests

Added PluginTests/TrickyLicense, a local fixture package referenced from ExamplePackage via .package(path:). Its LICENSE deliberately contains \, \n, \q, \(, \#n, """ (both on its own line and mid-line) and """#.

ExamplePackageTests:

  • adds TrickyLicense to the existing exact license-name list (now 29 entries),
  • asserts the fixture still contains each hostile construct, so the test cannot silently become vacuous if the fixture is ever tidied up,
  • asserts the generated licenseText reproduces the fixture file byte-for-byte.

The regression test is load-bearing — I verified both failure modes:

  • reverting only the change to LicensesPlugin.swift makes swift build in PluginTests/ExamplePackage fail with invalid escape sequence in literal and multi-line string literal content must begin on a new line;
  • hardcoding the delimiter to a single # also fails, on \#( and """# — this is what the computed count prevents.

Scope

Deliberately limited to the escaping fix and its regression test. Two things I noticed but left alone, to keep this reviewable:

  • id and name are also interpolated into plain "…" literals without escaping. Package identities and display names cannot realistically contain " or \, so I did not touch them.
  • The build emits Path/pluginWorkDirectory deprecation warnings. Unrelated to this change; happy to send that separately.

@yutailang0119
yutailang0119 marked this pull request as draft August 18, 2026 12:35
@yutailang0119
yutailang0119 force-pushed the fix/escape-generated-license-text branch from 4b90afd to 963fe05 Compare August 18, 2026 12:40
The generated `Licenses+Generated.swift` embedded each LICENSE body into a
plain multiline string literal, so a license text containing a backslash or
a triple quote produced a file that does not compile. The failure surfaces
while compiling the *consuming* target, which makes it hard to attribute to
this plugin.

Emit a raw string literal instead, and widen the `#` delimiter when the
license text itself contains a sequence that would otherwise close it. A
single `#` is not enough on its own: a raw literal delimited by one `#`
still ends at `"""#` and still interprets `\#` as an escape.

Add `PluginTests/TrickyLicense`, a local fixture package whose LICENSE
contains those constructs, and assert that the generated `licenseText`
reproduces it verbatim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yutailang0119
yutailang0119 force-pushed the fix/escape-generated-license-text branch from 963fe05 to cdec71c Compare August 18, 2026 14:59
@yutailang0119
yutailang0119 marked this pull request as ready for review August 18, 2026 15:34
@yutailang0119
yutailang0119 marked this pull request as draft August 18, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant