// // Reads and writes Apple Container's system configuration. // // The default DNS domain -- the suffix every container's hostname gets -- lives // in config.toml, and the CLI has no setter for it (`container system property` // only lists). The file is small and hand-editable, so it is edited in place. // const { exec } = require("./Process"); const RELATIVE_PATH = "Library/Application Support/com.apple.container/config/config.toml"; function path() { let home = nova.environment.HOME; if (!home) { return null; } return nova.path.join(home, RELATIVE_PATH); } function read() { let file = path(); if (!file || !nova.fs.access(file, nova.fs.R_OK)) { return ""; } try { let handle = nova.fs.open(file, "r"); let contents = handle.read(); handle.close(); return contents || ""; } catch (error) { return ""; } } // Locates the [dns] table's body within a set of lines. A table runs until the // next table header, which is a line that *starts* with "[" -- an array value // such as `nameservers = ["1.1.1.1"]` does not end it. function dnsTable(lines) { let start = lines.findIndex((line) => /^\s*\[dns\]\s*$/.test(line)); if (start === -1) { return null; } let end = lines.findIndex((line, index) => index > start && /^\s*\[/.test(line)); return { start: start, end: end === -1 ? lines.length : end }; } // The configured default DNS domain, or null when containers get no suffix. function defaultDomain() { let lines = read().split("\n"); let table = dnsTable(lines); if (!table) { return null; } for (let line of lines.slice(table.start + 1, table.end)) { let match = line.match(/^\s*domain\s*=\s*["']([^"']*)["']/); if (match) { return match[1].length > 0 ? match[1] : null; } } return null; } // Rewrites the `domain` key inside the `[dns]` table, adding either as needed. // Passing null removes the key, which restores unsuffixed hostnames. function withDomain(contents, domain) { let assignment = domain === null ? "" : `domain = "${domain}"\n`; let lines = contents.length > 0 ? contents.split("\n") : []; let table = dnsTable(lines); if (!table) { if (domain === null) { return contents; } let prefix = contents.length > 0 && !contents.endsWith("\n") ? `${contents}\n` : contents; return `${prefix}\n[dns]\n${assignment}`; } let body = lines.slice(table.start + 1, table.end); let existing = body.findIndex((line) => /^\s*domain\s*=/.test(line)); if (existing === -1) { if (domain !== null) { body.unshift(assignment.trimEnd()); } } else if (domain === null) { body.splice(existing, 1); } else { body[existing] = assignment.trimEnd(); } return lines .slice(0, table.start + 1) .concat(body, lines.slice(table.end)) .join("\n"); } async function setDefaultDomain(domain) { let file = path(); if (!file) { throw new Error("Could not locate Apple Container's configuration directory."); } let contents = nova.fs.access(file, nova.fs.F_OK) ? read() : ""; let updated = withDomain(contents, domain); // The file ships read-only, so widen it, write, and put the mode back. let existed = nova.fs.access(file, nova.fs.F_OK); if (existed) { await exec("/bin/chmod", ["u+w", file]); } else { let directory = nova.path.dirname(file); if (!nova.fs.access(directory, nova.fs.F_OK)) { nova.fs.mkdir(directory); } } let handle = nova.fs.open(file, "w"); handle.write(updated); handle.close(); await exec("/bin/chmod", ["444", file]); return updated; } module.exports = { path, read, defaultDomain, setDefaultDomain, withDomain };