diff --git a/tools/packaging/verify-linux-frame-copy-runtime.mjs b/tools/packaging/verify-linux-frame-copy-runtime.mjs index de6b9c082..afb5bce81 100644 --- a/tools/packaging/verify-linux-frame-copy-runtime.mjs +++ b/tools/packaging/verify-linux-frame-copy-runtime.mjs @@ -464,6 +464,76 @@ function yamlSequenceIncludes(lines, entry, expected) { ); } +function yamlSyntaxOutsideQuotedScalars(line) { + let result = ''; + let quote = null; + for (let index = 0; index < line.length; index += 1) { + const character = line[index]; + if (quote === "'") { + if (character === "'" && line[index + 1] === "'") { + result += ' '; + index += 1; + } else { + result += ' '; + if (character === "'") { + quote = null; + } + } + continue; + } + if (quote === '"') { + result += ' '; + if (character === '\\') { + result += ' '; + index += 1; + } else if (character === '"') { + quote = null; + } + continue; + } + if (character === '#') { + break; + } + if (character === "'" || character === '"') { + quote = character; + result += ' '; + continue; + } + result += character; + } + return result; +} + +function containsUnsupportedYamlSemantics(lines) { + let blockScalarParentIndent = null; + for (const line of lines) { + if (!line.trim()) { + continue; + } + const lineIndent = line.length - line.trimStart().length; + if ( + blockScalarParentIndent !== null && + lineIndent > blockScalarParentIndent + ) { + continue; + } + blockScalarParentIndent = null; + const syntax = yamlSyntaxOutsideQuotedScalars(line); + if ( + /(?:^|[\s:[\]{},])(?:[&*][A-Za-z0-9_-]+|![^\s,[\]{}]+)(?=$|[\s,\]}])/.test( + syntax + ) || + /(?:^|\s)<<\s*:/.test(syntax) + ) { + return true; + } + if (/:\s*[>|][+-]?\d?\s*$/.test(syntax)) { + blockScalarParentIndent = lineIndent; + } + } + return false; +} + export function validateExtractedSnapMetadata(extractionRoot) { const snapYamlPath = path.join(extractionRoot, 'meta', 'snap.yaml'); let stat; @@ -492,6 +562,11 @@ export function validateExtractedSnapMetadata(extractionRoot) { ]; } const lines = contents.split(/\r?\n/); + if (containsUnsupportedYamlSemantics(lines)) { + return [ + 'Extracted Snap metadata must not use YAML anchors, aliases, merge keys, or custom tags.', + ]; + } const errors = []; const plugs = singleYamlMappingEntry( lines, diff --git a/tools/packaging/verify-linux-frame-copy-runtime.test.mjs b/tools/packaging/verify-linux-frame-copy-runtime.test.mjs index 96e1a0228..49d9fc7a2 100644 --- a/tools/packaging/verify-linux-frame-copy-runtime.test.mjs +++ b/tools/packaging/verify-linux-frame-copy-runtime.test.mjs @@ -575,6 +575,8 @@ test('requires a private top-level shared-memory plug used by the Snap app', () const validSnapYaml = [ 'name: iptvnator', + 'summary: "*literal &anchor !tag <<: is quoted"', + '# *commented-alias &commented-anchor !commented-tag', 'apps:', ' iptvnator:', ' command: iptvnator', @@ -660,6 +662,45 @@ test('requires a private top-level shared-memory plug used by the Snap app', () ), /plug keys.*unique/i, ], + [ + (contents) => + [ + 'shared-interface: &sharedInterface shared-memory', + contents.replace( + '\nplugs:\n', + '\nplugs:\n alias-plug:\n interface: *sharedInterface\n' + ), + ].join('\n'), + /anchors, aliases, merge keys, or custom tags/i, + ], + [ + (contents) => + [ + 'shared-interface: &sharedInterface shared-memory', + `${contents}slots:\n alias-slot:\n interface: *sharedInterface\n`, + ].join('\n'), + /anchors, aliases, merge keys, or custom tags/i, + ], + [ + (contents) => + contents.replace( + '\nplugs:\n', + '\nplugs:\n tagged:\n interface: !shared-memory shared-memory\n' + ), + /anchors, aliases, merge keys, or custom tags/i, + ], + [ + (contents) => + [ + 'shared-plug: &sharedPlug', + ' interface: network', + contents.replace( + '\nplugs:\n', + '\nplugs:\n merged:\n <<: *sharedPlug\n' + ), + ].join('\n'), + /anchors, aliases, merge keys, or custom tags/i, + ], [ (contents) => contents.replace(