Skip to content

Commit 65e3681

Browse files
authored
fix: remove naive regex parsing of .gitmodules file (#15)
1 parent 867fdb1 commit 65e3681

File tree

9 files changed

+397
-52
lines changed

9 files changed

+397
-52
lines changed

dist/index.js

Lines changed: 302 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3072,6 +3072,293 @@ function copyFile(srcFile, destFile, force) {
30723072
}
30733073
//# sourceMappingURL=io.js.map
30743074

3075+
/***/ }),
3076+
3077+
/***/ 6202:
3078+
/***/ ((module) => {
3079+
3080+
const { hasOwnProperty } = Object.prototype
3081+
3082+
const encode = (obj, opt = {}) => {
3083+
if (typeof opt === 'string') {
3084+
opt = { section: opt }
3085+
}
3086+
opt.align = opt.align === true
3087+
opt.newline = opt.newline === true
3088+
opt.sort = opt.sort === true
3089+
opt.whitespace = opt.whitespace === true || opt.align === true
3090+
// The `typeof` check is required because accessing the `process` directly fails on browsers.
3091+
/* istanbul ignore next */
3092+
opt.platform = opt.platform || (typeof process !== 'undefined' && process.platform)
3093+
opt.bracketedArray = opt.bracketedArray !== false
3094+
3095+
/* istanbul ignore next */
3096+
const eol = opt.platform === 'win32' ? '\r\n' : '\n'
3097+
const separator = opt.whitespace ? ' = ' : '='
3098+
const children = []
3099+
3100+
const keys = opt.sort ? Object.keys(obj).sort() : Object.keys(obj)
3101+
3102+
let padToChars = 0
3103+
// If aligning on the separator, then padToChars is determined as follows:
3104+
// 1. Get the keys
3105+
// 2. Exclude keys pointing to objects unless the value is null or an array
3106+
// 3. Add `[]` to array keys
3107+
// 4. Ensure non empty set of keys
3108+
// 5. Reduce the set to the longest `safe` key
3109+
// 6. Get the `safe` length
3110+
if (opt.align) {
3111+
padToChars = safe(
3112+
(
3113+
keys
3114+
.filter(k => obj[k] === null || Array.isArray(obj[k]) || typeof obj[k] !== 'object')
3115+
.map(k => Array.isArray(obj[k]) ? `${k}[]` : k)
3116+
)
3117+
.concat([''])
3118+
.reduce((a, b) => safe(a).length >= safe(b).length ? a : b)
3119+
).length
3120+
}
3121+
3122+
let out = ''
3123+
const arraySuffix = opt.bracketedArray ? '[]' : ''
3124+
3125+
for (const k of keys) {
3126+
const val = obj[k]
3127+
if (val && Array.isArray(val)) {
3128+
for (const item of val) {
3129+
out += safe(`${k}${arraySuffix}`).padEnd(padToChars, ' ') + separator + safe(item) + eol
3130+
}
3131+
} else if (val && typeof val === 'object') {
3132+
children.push(k)
3133+
} else {
3134+
out += safe(k).padEnd(padToChars, ' ') + separator + safe(val) + eol
3135+
}
3136+
}
3137+
3138+
if (opt.section && out.length) {
3139+
out = '[' + safe(opt.section) + ']' + (opt.newline ? eol + eol : eol) + out
3140+
}
3141+
3142+
for (const k of children) {
3143+
const nk = splitSections(k, '.').join('\\.')
3144+
const section = (opt.section ? opt.section + '.' : '') + nk
3145+
const child = encode(obj[k], {
3146+
...opt,
3147+
section,
3148+
})
3149+
if (out.length && child.length) {
3150+
out += eol
3151+
}
3152+
3153+
out += child
3154+
}
3155+
3156+
return out
3157+
}
3158+
3159+
function splitSections (str, separator) {
3160+
var lastMatchIndex = 0
3161+
var lastSeparatorIndex = 0
3162+
var nextIndex = 0
3163+
var sections = []
3164+
3165+
do {
3166+
nextIndex = str.indexOf(separator, lastMatchIndex)
3167+
3168+
if (nextIndex !== -1) {
3169+
lastMatchIndex = nextIndex + separator.length
3170+
3171+
if (nextIndex > 0 && str[nextIndex - 1] === '\\') {
3172+
continue
3173+
}
3174+
3175+
sections.push(str.slice(lastSeparatorIndex, nextIndex))
3176+
lastSeparatorIndex = nextIndex + separator.length
3177+
}
3178+
} while (nextIndex !== -1)
3179+
3180+
sections.push(str.slice(lastSeparatorIndex))
3181+
3182+
return sections
3183+
}
3184+
3185+
const decode = (str, opt = {}) => {
3186+
opt.bracketedArray = opt.bracketedArray !== false
3187+
const out = Object.create(null)
3188+
let p = out
3189+
let section = null
3190+
// section |key = value
3191+
const re = /^\[([^\]]*)\]\s*$|^([^=]+)(=(.*))?$/i
3192+
const lines = str.split(/[\r\n]+/g)
3193+
const duplicates = {}
3194+
3195+
for (const line of lines) {
3196+
if (!line || line.match(/^\s*[;#]/) || line.match(/^\s*$/)) {
3197+
continue
3198+
}
3199+
const match = line.match(re)
3200+
if (!match) {
3201+
continue
3202+
}
3203+
if (match[1] !== undefined) {
3204+
section = unsafe(match[1])
3205+
if (section === '__proto__') {
3206+
// not allowed
3207+
// keep parsing the section, but don't attach it.
3208+
p = Object.create(null)
3209+
continue
3210+
}
3211+
p = out[section] = out[section] || Object.create(null)
3212+
continue
3213+
}
3214+
const keyRaw = unsafe(match[2])
3215+
let isArray
3216+
if (opt.bracketedArray) {
3217+
isArray = keyRaw.length > 2 && keyRaw.slice(-2) === '[]'
3218+
} else {
3219+
duplicates[keyRaw] = (duplicates?.[keyRaw] || 0) + 1
3220+
isArray = duplicates[keyRaw] > 1
3221+
}
3222+
const key = isArray && keyRaw.endsWith('[]')
3223+
? keyRaw.slice(0, -2) : keyRaw
3224+
3225+
if (key === '__proto__') {
3226+
continue
3227+
}
3228+
const valueRaw = match[3] ? unsafe(match[4]) : true
3229+
const value = valueRaw === 'true' ||
3230+
valueRaw === 'false' ||
3231+
valueRaw === 'null' ? JSON.parse(valueRaw)
3232+
: valueRaw
3233+
3234+
// Convert keys with '[]' suffix to an array
3235+
if (isArray) {
3236+
if (!hasOwnProperty.call(p, key)) {
3237+
p[key] = []
3238+
} else if (!Array.isArray(p[key])) {
3239+
p[key] = [p[key]]
3240+
}
3241+
}
3242+
3243+
// safeguard against resetting a previously defined
3244+
// array by accidentally forgetting the brackets
3245+
if (Array.isArray(p[key])) {
3246+
p[key].push(value)
3247+
} else {
3248+
p[key] = value
3249+
}
3250+
}
3251+
3252+
// {a:{y:1},"a.b":{x:2}} --> {a:{y:1,b:{x:2}}}
3253+
// use a filter to return the keys that have to be deleted.
3254+
const remove = []
3255+
for (const k of Object.keys(out)) {
3256+
if (!hasOwnProperty.call(out, k) ||
3257+
typeof out[k] !== 'object' ||
3258+
Array.isArray(out[k])) {
3259+
continue
3260+
}
3261+
3262+
// see if the parent section is also an object.
3263+
// if so, add it to that, and mark this one for deletion
3264+
const parts = splitSections(k, '.')
3265+
p = out
3266+
const l = parts.pop()
3267+
const nl = l.replace(/\\\./g, '.')
3268+
for (const part of parts) {
3269+
if (part === '__proto__') {
3270+
continue
3271+
}
3272+
if (!hasOwnProperty.call(p, part) || typeof p[part] !== 'object') {
3273+
p[part] = Object.create(null)
3274+
}
3275+
p = p[part]
3276+
}
3277+
if (p === out && nl === l) {
3278+
continue
3279+
}
3280+
3281+
p[nl] = out[k]
3282+
remove.push(k)
3283+
}
3284+
for (const del of remove) {
3285+
delete out[del]
3286+
}
3287+
3288+
return out
3289+
}
3290+
3291+
const isQuoted = val => {
3292+
return (val.startsWith('"') && val.endsWith('"')) ||
3293+
(val.startsWith("'") && val.endsWith("'"))
3294+
}
3295+
3296+
const safe = val => {
3297+
if (
3298+
typeof val !== 'string' ||
3299+
val.match(/[=\r\n]/) ||
3300+
val.match(/^\[/) ||
3301+
(val.length > 1 && isQuoted(val)) ||
3302+
val !== val.trim()
3303+
) {
3304+
return JSON.stringify(val)
3305+
}
3306+
return val.split(';').join('\\;').split('#').join('\\#')
3307+
}
3308+
3309+
const unsafe = val => {
3310+
val = (val || '').trim()
3311+
if (isQuoted(val)) {
3312+
// remove the single quotes before calling JSON.parse
3313+
if (val.charAt(0) === "'") {
3314+
val = val.slice(1, -1)
3315+
}
3316+
try {
3317+
val = JSON.parse(val)
3318+
} catch {
3319+
// ignore errors
3320+
}
3321+
} else {
3322+
// walk the val to find the first not-escaped ; character
3323+
let esc = false
3324+
let unesc = ''
3325+
for (let i = 0, l = val.length; i < l; i++) {
3326+
const c = val.charAt(i)
3327+
if (esc) {
3328+
if ('\\;#'.indexOf(c) !== -1) {
3329+
unesc += c
3330+
} else {
3331+
unesc += '\\' + c
3332+
}
3333+
3334+
esc = false
3335+
} else if (';#'.indexOf(c) !== -1) {
3336+
break
3337+
} else if (c === '\\') {
3338+
esc = true
3339+
} else {
3340+
unesc += c
3341+
}
3342+
}
3343+
if (esc) {
3344+
unesc += '\\'
3345+
}
3346+
3347+
return unesc.trim()
3348+
}
3349+
return val
3350+
}
3351+
3352+
module.exports = {
3353+
parse: decode,
3354+
decode,
3355+
stringify: encode,
3356+
encode,
3357+
safe,
3358+
unsafe,
3359+
}
3360+
3361+
30753362
/***/ }),
30763363

30773364
/***/ 4225:
@@ -30675,7 +30962,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
3067530962
});
3067630963
};
3067730964
Object.defineProperty(exports, "__esModule", ({ value: true }));
30678-
exports.setDynamicOutputs = exports.updateToLatestTag = exports.updateToLatestCommit = exports.filterSubmodules = exports.parseGitmodules = exports.parseInputs = void 0;
30965+
exports.setDynamicOutputs = exports.updateToLatestTag = exports.updateToLatestCommit = exports.filterSubmodules = exports.parseGitmodules = exports.readFile = exports.parseInputs = void 0;
3067930966
exports.run = run;
3068030967
const exec_1 = __nccwpck_require__(7775);
3068130968
const core = __importStar(__nccwpck_require__(9093));
@@ -30684,11 +30971,16 @@ const logging_1 = __nccwpck_require__(805);
3068430971
const markdown_1 = __nccwpck_require__(5094);
3068530972
const zod_1 = __nccwpck_require__(8009);
3068630973
const git_1 = __nccwpck_require__(3555);
30974+
const ini_1 = __nccwpck_require__(6202);
3068730975
const updateStrategy = zod_1.z.enum(["commit", "tag"]);
30976+
const gitmodulesSchema = zod_1.z.record(zod_1.z.string(), zod_1.z.object({
30977+
path: zod_1.z.string(),
30978+
url: zod_1.z.string().url(),
30979+
}));
3068830980
const parseInputs = () => __awaiter(void 0, void 0, void 0, function* () {
3068930981
const gitmodulesPath = core.getInput("gitmodulesPath").trim();
3069030982
const inputSubmodules = core.getInput("submodules").trim();
30691-
const strategy = yield updateStrategy.parseAsync(core.getInput("strategy"));
30983+
const strategy = yield updateStrategy.parseAsync(core.getInput("strategy").trim());
3069230984
// Github Actions doesn't support array inputs, so submodules must be separated by newlines
3069330985
const parsedSubmodules = inputSubmodules
3069430986
.split("\n")
@@ -30717,10 +31009,14 @@ const readFile = (path) => __awaiter(void 0, void 0, void 0, function* () {
3071731009
throw error;
3071831010
});
3071931011
});
31012+
exports.readFile = readFile;
3072031013
const parseGitmodules = (content) => __awaiter(void 0, void 0, void 0, function* () {
30721-
const gitmodulesRegex = /^\s*\[submodule\s+"([^"]+)"\]\s*\n\s*path\s*=\s*(.+)\s*\n\s*url\s*=\s*(.+)\s*$/gm;
30722-
const parsedContent = Array.from(content.matchAll(gitmodulesRegex));
30723-
const detectedSubmodules = yield Promise.all(parsedContent.map((_a) => __awaiter(void 0, [_a], void 0, function* ([_, name, path, url]) {
31014+
const parsed = (0, ini_1.parse)(content);
31015+
const gitmodules = yield gitmodulesSchema.parseAsync(parsed);
31016+
return yield Promise.all(Object.entries(gitmodules).map((_a) => __awaiter(void 0, [_a], void 0, function* ([key, values]) {
31017+
const name = key.split('"')[1].trim();
31018+
const path = values.path.replace(/"/g, "").trim();
31019+
const url = values.url.replace(/"/g, "").trim();
3072431020
const [previousCommitSha, previousShortCommitSha] = yield (0, git_1.getCommit)(path);
3072531021
const previousTag = yield (0, git_1.getPreviousTag)(path);
3072631022
return {
@@ -30736,7 +31032,6 @@ const parseGitmodules = (content) => __awaiter(void 0, void 0, void 0, function*
3073631032
previousTag,
3073731033
};
3073831034
})));
30739-
return detectedSubmodules;
3074031035
});
3074131036
exports.parseGitmodules = parseGitmodules;
3074231037
const filterSubmodules = (inputSubmodules, detectedSubmodules, strategy) => __awaiter(void 0, void 0, void 0, function* () {
@@ -30823,7 +31118,7 @@ function run() {
3082331118
return __awaiter(this, void 0, void 0, function* () {
3082431119
try {
3082531120
const { gitmodulesPath, inputSubmodules, strategy } = yield (0, exports.parseInputs)();
30826-
const gitmodulesContent = yield readFile(gitmodulesPath);
31121+
const gitmodulesContent = yield (0, exports.readFile)(gitmodulesPath);
3082731122
if (gitmodulesContent === "") {
3082831123
core.info("No submodules detected.");
3082931124
core.info("Nothing to do. Exiting...");

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/licenses.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,25 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
8181
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
8282
IN THE SOFTWARE.
8383

84+
ini
85+
ISC
86+
The ISC License
87+
88+
Copyright (c) Isaac Z. Schlueter and Contributors
89+
90+
Permission to use, copy, modify, and/or distribute this software for any
91+
purpose with or without fee is hereby granted, provided that the above
92+
copyright notice and this permission notice appear in all copies.
93+
94+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
95+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
96+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
97+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
98+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
99+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
100+
IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
101+
102+
84103
tunnel
85104
MIT
86105
The MIT License (MIT)

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
"dependencies": {
3737
"@actions/core": "^1.10.1",
3838
"@actions/exec": "^1.1.1",
39+
"ini": "^4.1.3",
3940
"zod": "^3.23.8"
4041
},
4142
"devDependencies": {
4243
"@tsconfig/recommended": "^1.0.7",
44+
"@types/ini": "^4.1.1",
4345
"@types/node": "^22.2.0",
4446
"@vercel/ncc": "^0.38.1",
4547
"@vitest/coverage-v8": "^2.0.5",

0 commit comments

Comments
 (0)