Added lots of testcases

This commit is contained in:
Benjamin Bädorf 2020-11-25 14:26:24 +01:00
parent b78122ac70
commit 1f207fd7c8
No known key found for this signature in database
GPG key ID: 4406E80E13CD656C
38 changed files with 1505 additions and 169 deletions

1
README.md Executable file
View file

@ -0,0 +1 @@
# unitfile-parser

0
package-lock.json generated Normal file → Executable file
View file

2
package.json Normal file → Executable file
View file

@ -5,7 +5,7 @@
"main": "src/mod.mjs",
"type": "module",
"scripts": {
"test": "node ./test/spec.mjs './test/examples/*.mjs'"
"test": "./scripts/run-tests"
},
"author": "",
"license": "ISC",

8
scripts/run-tests Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -e
node ./src/spec/string-to-ast.mjs;
node ./src/spec/ast-to-data.mjs;
node ./src/spec/data-to-ast.mjs;
node ./src/spec/ast-to-string.mjs;

35
src/Lexer.mjs Normal file → Executable file
View file

@ -1,15 +1,14 @@
import chevrotain from "chevrotain";
const { createToken, Lexer } = chevrotain;
function getValueParser(...stoppers) {
return function exec(text, startOffset) {
function valueParser(text, startOffset) {
for (let i = startOffset; i < text.length; i += 1) {
if (text[i] === '\\') {
i += 1;
continue;
}
if (stoppers.includes(text[i])) {
if (['\n', '#'].includes(text[i])) {
if (i !== startOffset) {
return [text.substring(startOffset, i)];
} else {
@ -20,7 +19,6 @@ function getValueParser(...stoppers) {
return [text.substr(startOffset)];
}
}
export const SectionHeading = createToken({
name: "SectionHeading",
@ -33,53 +31,44 @@ export const Property = createToken({
});
export const Value = createToken({
name: "Value",
pattern: { exec: getValueParser('\n', '#') },
pattern: { exec: valueParser },
line_breaks: true,
pop_mode: true,
});
export const CommentStartNewline = createToken({
name: "CommentStartNewline",
pattern: '\n#',
push_mode: "comment_mode",
});
export const CommentStart = createToken({
name: "CommentStart",
pattern: '#',
push_mode: "comment_mode",
});
export const Comment = createToken({
name: "Comment",
pattern: { exec: getValueParser('\n') },
line_breaks: true,
pop_mode: true,
pattern: /#.*/,
line_breaks: false,
});
export const WhiteSpace = createToken({
name: "WhiteSpace",
pattern: /\s+/,
group: Lexer.SKIPPED,
});
export const Newline = createToken({
name: "Newline",
pattern: /\n+/,
});
export const tokens = [
CommentStart,
CommentStartNewline,
Value,
Comment,
SectionHeading,
Property,
WhiteSpace,
Newline,
];
export default new Lexer({
defaultMode: "line_mode",
modes: {
line_mode: [
CommentStartNewline,
CommentStart,
Comment,
SectionHeading,
Property,
Newline,
WhiteSpace,
],
value_mode: [ Value ],
comment_mode: [ Comment ],
},
});

22
src/Parser.mjs Normal file → Executable file
View file

@ -3,10 +3,9 @@ const { CstParser } = chevrotain;
import {
tokens,
Comment,
CommentStart,
CommentStartNewline,
Property,
SectionHeading,
Newline,
Value,
} from "./Lexer.mjs";
@ -15,25 +14,22 @@ export default class UnitFileParser extends CstParser {
super(tokens);
const $ = this;
$.RULE("commentLine", () => {
$.CONSUME(CommentStartNewline);
$.CONSUME(Comment);
$.RULE("value", () => {
$.CONSUME(Value);
});
$.RULE("comment", () => {
$.CONSUME(CommentStart);
$.CONSUME(Comment);
});
$.RULE("value", () => {
$.CONSUME(Value);
});
$.RULE("sectionHeadingStatement", () => {
$.CONSUME(SectionHeading);
$.OPTION(() => {
$.SUBRULE($.comment);
});
$.OPTION1(() => {
$.CONSUME(Newline);
});
});
$.RULE("propertyStatement", () => {
@ -47,7 +43,7 @@ export default class UnitFileParser extends CstParser {
$.RULE("sectionStatement", () => {
$.OR([
{ ALT: () => $.SUBRULE($.propertyStatement) },
{ ALT: () => $.SUBRULE($.commentLine) },
{ ALT: () => $.SUBRULE($.comment) },
]);
});
@ -55,6 +51,9 @@ export default class UnitFileParser extends CstParser {
$.SUBRULE($.sectionHeadingStatement);
$.MANY(() => {
$.SUBRULE($.sectionStatement);
$.OPTION(() => {
$.CONSUME(Newline);
});
});
});
@ -63,6 +62,7 @@ export default class UnitFileParser extends CstParser {
$.OR([
{ ALT: () => $.SUBRULE($.section) },
{ ALT: () => $.SUBRULE($.comment) },
{ ALT: () => $.CONSUME(Newline) },
]);
});
});

37
src/ast-to-data.mjs Executable file
View file

@ -0,0 +1,37 @@
function nodeToData(node) {
switch (node.type) {
case "unitFile":
return node.sections.map(nodeToData);
case "section":
return {
title: node.title,
settings: node.body
.filter(n => n.type === "setting")
.reduce(
(acc, setting) => {
if (acc[setting.name]) {
return {
...acc,
[setting.name]: [acc[setting.name], setting.value].flat(),
};
}
return {
...acc,
[setting.name]: nodeToData(setting),
};
},
{},
),
};
case "setting":
return node.value;
default:
throw new Error(`Unrecognized node type: ${node.type}`);
break;
}
}
export default function astToData(file) {
return nodeToData(file);
}

23
src/ast-to-string.mjs Executable file
View file

@ -0,0 +1,23 @@
function nodeToString(node) {
switch (node.type) {
case "unitFile":
return `${(node.comments || []).map(nodeToString).join('\n')}
${(node.sections || []).map(nodeToString).join('\n')}`;
case "comment":
return `# ${comment.value}`;
case "section":
const titleComment = ` ${node.titleComment ? nodeToString(node.titleComment) : ''}`;
return `[${node.title}]${titleComment}
${(node.body || []).map(nodeToString).join('\n')}`;
case "setting":
const comment = ` ${node.comment ? nodeToString(node.comment) : ''}`;
return `${node.name}=${node.value}${comment}`;
default:
throw new Error(`Unrecognized node type: ${node.type}`);
break;
}
}
export default function astToString(file) {
return nodeToString(file);
}

40
src/data-to-ast.mjs Executable file
View file

@ -0,0 +1,40 @@
function sectionToAst(section) {
return {
type: "section",
title: section.title,
body: Object.keys(section.settings)
.reduce(
(acc, key) => {
const val = section.settings[key];
if (Array.isArray(val)) {
return [
...acc,
...val.map(value => ({
type: "setting",
name: key,
value,
})),
];
}
return [
...acc,
{
type: "setting",
name: key,
value: val,
},
];
},
[],
),
};
}
export default function dataToAst(file) {
return {
type: "unitFile",
comments: [],
sections: file.map(sectionToAst),
};
}

6
src/data-to-string.mjs Executable file
View file

@ -0,0 +1,6 @@
import astToString from "./ast-to-string.mjs";
import dataToAst from "./data-to-ast.mjs";
export default function dataToString(data) {
return astToString(dataToAst(data));
}

35
src/mod.mjs Normal file → Executable file
View file

@ -1,30 +1,11 @@
export { default as Lexer } from "./Lexer.mjs";
export { default as Parser } from "./Parser.mjs";
export { default as visitorCreator } from "./visitor.mjs";
export { default as visitorCreator } from "./visitor-creator.mjs";
import Parser from "./Parser.mjs";
import Lexer from "./Lexer.mjs";
import visitorCreator from "./visitor.mjs";
export default (input) => {
const parser = new Parser([], { outputCst: true });
const lexingresult = Lexer.tokenize(input);
if (lexingresult.errors.length > 0) {
console.dir(lexingresult, { depth: Infinity });
throw new Error("Lexing errors detected")
}
parser.input = lexingresult.tokens;
const cst = parser.unitFile();
if (parser.errors.length > 0) {
console.dir(parser.errors, { depth: Infinity });
throw new Error("Parsing errors detected")
}
const Visitor = visitorCreator(parser);
const visitor = new Visitor();
const ast = visitor.visit(cst);
return ast;
}
export { default as stringToAst } from "./string-to-ast.mjs";
export { default as stringToData } from "./string-to-data.mjs";
export { default as astToData } from "./ast-to-data.mjs";
export { default as astToString } from "./ast-to-string.mjs";
export { default as astToSTring } from "./ast-to-string.mjs";
export { default as dataToAst } from "./data-to-ast.mjs";
export { default as dataToString } from "./data-to-string.mjs";

44
src/spec/ast-to-data.mjs Executable file
View file

@ -0,0 +1,44 @@
import astToData from "../ast-to-data.mjs";
function runTest(input, expectedOutput) {
const result = astToData(input);
const expectedOutputString = JSON.stringify(expectedOutput);
const resultString = JSON.stringify(result);
if (expectedOutputString !== resultString) {
console.dir(input, { depth: Infinity });
throw new Error(`Mismatching result on testcase:
=======Result======
${resultString}
=======Expected======
${expectedOutputString}\n\n`);
}
}
(async () => {
const cases = await Promise.all([
"./parser/basic.mjs",
"./parser/comments.mjs",
"./parser/comment-only.mjs",
"./parser/multiline-value.mjs",
"./parser/weird-characters.mjs",
"./parser/repeated-settings.mjs",
"./parser/real.nix-daemon.mjs",
"./parser/real.maia-console@.mjs",
/*
"./parser/real.dbus-org.bluez.mjs",
"./parser/real.display-manager.mjs",
"./parser/real.systemd-fsck-silent@.mjs",
"./parser/real.systemd-fsck-silent-root.mjs",
"./parser/real.dbus-org.freedesktop.Avahi.mjs",
"./parser/real.dbus-org.freedesktop.ModemManager1.mjs",
"./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs",
*/
].map(file => import(file)));
const results = cases.forEach(({ ast, data }) => runTest(ast, data));
})();

View file

@ -1,7 +1,4 @@
export const content = `[Unit]
Description=Idle manager for Wayland`;
export const result = {
export const input = {
type: 'unitFile',
comments: [],
sections: [
@ -19,3 +16,12 @@ export const result = {
}
]
};
export const output = [
{
title: 'Unit',
settings: {
Description: 'Idle manager for Wayland',
},
}
];

View file

@ -0,0 +1,2 @@
export const input = ;
export const result = ;

0
src/spec/ast-to-string.mjs Executable file
View file

44
src/spec/data-to-ast.mjs Executable file
View file

@ -0,0 +1,44 @@
import astToData from "../data-to-ast.mjs";
function runTest(input, expectedOutput) {
const result = astToData(input);
const expectedOutputString = JSON.stringify(expectedOutput);
const resultString = JSON.stringify(result);
if (expectedOutputString !== resultString) {
console.dir(input, { depth: Infinity });
throw new Error(`Mismatching result on testcase:
=======Result======
${resultString}
=======Expected======
${expectedOutputString}\n\n`);
}
}
(async () => {
const cases = await Promise.all([
"./parser/basic.mjs",
"./parser/comments.mjs",
"./parser/comment-only.mjs",
"./parser/multiline-value.mjs",
"./parser/weird-characters.mjs",
"./parser/repeated-settings.mjs",
"./parser/real.nix-daemon.mjs",
"./parser/real.maia-console@.mjs",
/*
"./parser/real.dbus-org.bluez.mjs",
"./parser/real.display-manager.mjs",
"./parser/real.systemd-fsck-silent@.mjs",
"./parser/real.systemd-fsck-silent-root.mjs",
"./parser/real.dbus-org.freedesktop.Avahi.mjs",
"./parser/real.dbus-org.freedesktop.ModemManager1.mjs",
"./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs",
*/
].map(file => import(file)));
const results = cases.forEach(({ ast, data }) => runTest(ast, data));
})();

0
src/spec/data-to-string.mjs Executable file
View file

30
src/spec/parser/basic.mjs Executable file
View file

@ -0,0 +1,30 @@
export const string = `[Unit]
Description=Idle manager for Wayland`;
export const ast = {
type: 'unitFile',
comments: [],
sections: [
{
type: 'section',
title: 'Unit',
titleComment: undefined,
body: [
{
type: 'setting',
name: 'Description',
value: 'Idle manager for Wayland'
}
]
}
]
};
export const data = [
{
title: 'Unit',
settings: {
Description: 'Idle manager for Wayland',
},
}
];

View file

@ -0,0 +1,9 @@
export const string = `# Start of file comment`;
export const ast = {
type: 'unitFile',
comments: [ { type: 'comment', value: 'Start of file comment' } ],
sections: [],
};
export const data = [];

View file

@ -1,4 +1,5 @@
export const content = `# Outside comment
export const string = `# Outside comment
# Outside comment part 2
[Unit] # Heading comment
Description=Idle manager for Wayland # End of value comment
# Inline comment
@ -10,9 +11,12 @@ Alias=asdf#Comment Without spaces
# Comment only in this body
`;
export const result = {
export const ast = {
type: 'unitFile',
comments: [ { type: 'comment', value: 'Outside comment' } ],
comments: [
{ type: 'comment', value: 'Outside comment' },
{ type: 'comment', value: 'Outside comment part 2' },
],
sections: [
{
type: 'section',
@ -50,3 +54,18 @@ export const result = {
}
]
};
export const data = [
{
title: 'Unit',
settings: {
Description: 'Idle manager for Wayland',
ExecStart: 'echo "some string"',
Alias: 'asdf',
},
},
{
title: 'Install',
settings: {},
}
];

View file

@ -1,4 +1,4 @@
export const content = `
export const string = `
[Service]
ExecStart=/usr/bin/swayidle -w \\
timeout 600 'swaylock-bg' \\
@ -11,7 +11,7 @@ ExecPause=/usr/bin/swayidle -w \\
after-resume 'swaylock-bg'#No space comment
`;
export const result = {
export const ast = {
type: 'unitFile',
comments: [],
sections: [
@ -46,3 +46,18 @@ export const result = {
}
]
};
export const data = [
{
title: 'Service',
settings: {
ExecStart: '/usr/bin/swayidle -w \\\n' +
" timeout 600 'swaylock-bg' \\\n" +
` timeout 900 'swaymsg "output * dpms off"' \\\n` +
` resume 'swaymsg "output * dpms on"' \\\n` +
" after-resume 'swaylock-bg'",
ExecStop: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'",
ExecPause: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'",
},
},
];

View file

@ -0,0 +1,118 @@
export const string = `[Unit]
Description=Bluetooth service
Documentation=man:bluetoothd(8)
ConditionPathIsDirectory=/sys/class/bluetooth
[Service]
Type=dbus
BusName=org.bluez
ExecStart=/usr/lib/bluetooth/bluetoothd
NotifyAccess=main
#WatchdogSec=10
#Restart=on-failure
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
LimitNPROC=1
ProtectHome=true
ProtectSystem=full
[Install]
WantedBy=bluetooth.target
Alias=dbus-org.bluez.service`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "Bluetooth service"
},
{
"type": "setting",
"name": "Documentation",
"value": "man:bluetoothd(8)"
},
{
"type": "setting",
"name": "ConditionPathIsDirectory",
"value": "/sys/class/bluetooth"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "dbus"
},
{
"type": "setting",
"name": "BusName",
"value": "org.bluez"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/lib/bluetooth/bluetoothd"
},
{
"type": "setting",
"name": "NotifyAccess",
"value": "main"
},
{
"type": "comment",
"value": "WatchdogSec=10"
},
{
"type": "comment",
"value": "Restart=on-failure"
},
{
"type": "setting",
"name": "CapabilityBoundingSet",
"value": "CAP_NET_ADMIN CAP_NET_BIND_SERVICE"
},
{
"type": "setting",
"name": "LimitNPROC",
"value": "1"
},
{
"type": "setting",
"name": "ProtectHome",
"value": "true"
},
{
"type": "setting",
"name": "ProtectSystem",
"value": "full"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "WantedBy",
"value": "bluetooth.target"
},
{
"type": "setting",
"name": "Alias",
"value": "dbus-org.bluez.service"
}
]
}
]
};

View file

@ -0,0 +1,172 @@
export const string = `# This file is part of avahi.
#
# avahi is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation; either version 2 of the
# License, or (at your option) any later version.
#
# avahi is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
# License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with avahi; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA.
[Unit]
Description=Avahi mDNS/DNS-SD Stack
Requires=avahi-daemon.socket
[Service]
Type=dbus
BusName=org.freedesktop.Avahi
ExecStart=/usr/bin/avahi-daemon -s
ExecReload=/usr/bin/avahi-daemon -r
NotifyAccess=main
[Install]
WantedBy=multi-user.target
Also=avahi-daemon.socket
Alias=dbus-org.freedesktop.Avahi.service`
export const ast = {
"type": "unitFile",
"comments": [
{
"type": "comment",
"value": "This file is part of avahi."
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": "avahi is free software; you can redistribute it and/or modify it"
},
{
"type": "comment",
"value": "under the terms of the GNU Lesser General Public License as"
},
{
"type": "comment",
"value": "published by the Free Software Foundation; either version 2 of the"
},
{
"type": "comment",
"value": "License, or (at your option) any later version."
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": "avahi is distributed in the hope that it will be useful, but WITHOUT"
},
{
"type": "comment",
"value": "ANY WARRANTY; without even the implied warranty of MERCHANTABILITY"
},
{
"type": "comment",
"value": "or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public"
},
{
"type": "comment",
"value": "License for more details."
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": "You should have received a copy of the GNU Lesser General Public"
},
{
"type": "comment",
"value": "License along with avahi; if not, write to the Free Software"
},
{
"type": "comment",
"value": "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307"
},
{
"type": "comment",
"value": "USA."
}
],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "Avahi mDNS/DNS-SD Stack"
},
{
"type": "setting",
"name": "Requires",
"value": "avahi-daemon.socket"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "dbus"
},
{
"type": "setting",
"name": "BusName",
"value": "org.freedesktop.Avahi"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/bin/avahi-daemon -s"
},
{
"type": "setting",
"name": "ExecReload",
"value": "/usr/bin/avahi-daemon -r"
},
{
"type": "setting",
"name": "NotifyAccess",
"value": "main"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "WantedBy",
"value": "multi-user.target"
},
{
"type": "setting",
"name": "Also",
"value": "avahi-daemon.socket"
},
{
"type": "setting",
"name": "Alias",
"value": "dbus-org.freedesktop.Avahi.service"
}
]
}
]
};

View file

@ -0,0 +1,132 @@
export const string = `[Unit]
Description=Modem Manager
After=polkit.service
Requires=polkit.service
[Service]
Type=dbus
BusName=org.freedesktop.ModemManager1
ExecStart=/usr/bin/ModemManager
StandardError=null
Restart=on-abort
CapabilityBoundingSet=CAP_SYS_ADMIN
ProtectSystem=true
ProtectHome=true
PrivateTmp=true
RestrictAddressFamilies=AF_NETLINK AF_UNIX
NoNewPrivileges=true
User=root
[Install]
WantedBy=multi-user.target
Alias=dbus-org.freedesktop.ModemManager1.service`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "Modem Manager"
},
{
"type": "setting",
"name": "After",
"value": "polkit.service"
},
{
"type": "setting",
"name": "Requires",
"value": "polkit.service"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "dbus"
},
{
"type": "setting",
"name": "BusName",
"value": "org.freedesktop.ModemManager1"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/bin/ModemManager"
},
{
"type": "setting",
"name": "StandardError",
"value": "null"
},
{
"type": "setting",
"name": "Restart",
"value": "on-abort"
},
{
"type": "setting",
"name": "CapabilityBoundingSet",
"value": "CAP_SYS_ADMIN"
},
{
"type": "setting",
"name": "ProtectSystem",
"value": "true"
},
{
"type": "setting",
"name": "ProtectHome",
"value": "true"
},
{
"type": "setting",
"name": "PrivateTmp",
"value": "true"
},
{
"type": "setting",
"name": "RestrictAddressFamilies",
"value": "AF_NETLINK AF_UNIX"
},
{
"type": "setting",
"name": "NoNewPrivileges",
"value": "true"
},
{
"type": "setting",
"name": "User",
"value": "root"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "WantedBy",
"value": "multi-user.target"
},
{
"type": "setting",
"name": "Alias",
"value": "dbus-org.freedesktop.ModemManager1.service"
}
]
}
]
};

View file

@ -0,0 +1,78 @@
export const string = `[Unit]
Description=Network Manager Script Dispatcher Service
[Service]
Type=dbus
BusName=org.freedesktop.nm_dispatcher
ExecStart=/usr/lib/nm-dispatcher
# We want to allow scripts to spawn long-running daemons, so tell
# systemd to not clean up when nm-dispatcher exits
KillMode=process
[Install]
Alias=dbus-org.freedesktop.nm-dispatcher.service
`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "Network Manager Script Dispatcher Service"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "dbus"
},
{
"type": "setting",
"name": "BusName",
"value": "org.freedesktop.nm_dispatcher"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/lib/nm-dispatcher"
},
{
"type": "comment",
"value": "We want to allow scripts to spawn long-running daemons, so tell"
},
{
"type": "comment",
"value": "systemd to not clean up when nm-dispatcher exits"
},
{
"type": "setting",
"name": "KillMode",
"value": "process"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "Alias",
"value": "dbus-org.freedesktop.nm-dispatcher.service"
}
]
}
]
};

View file

@ -0,0 +1,90 @@
export const string = `[Unit]
Description=TUI display manager
After=systemd-user-sessions.service plymouth-quit-wait.service
After=getty@tty2.service
[Service]
Type=idle
ExecStart=/usr/bin/ly
StandardInput=tty
TTYPath=/dev/tty2
TTYReset=yes
TTYVHangup=yes
[Install]
Alias=display-manager.service`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "TUI display manager"
},
{
"type": "setting",
"name": "After",
"value": "systemd-user-sessions.service plymouth-quit-wait.service"
},
{
"type": "setting",
"name": "After",
"value": "getty@tty2.service"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "idle"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/bin/ly"
},
{
"type": "setting",
"name": "StandardInput",
"value": "tty"
},
{
"type": "setting",
"name": "TTYPath",
"value": "/dev/tty2"
},
{
"type": "setting",
"name": "TTYReset",
"value": "yes"
},
{
"type": "setting",
"name": "TTYVHangup",
"value": "yes"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "Alias",
"value": "display-manager.service"
}
]
}
]
};

View file

@ -0,0 +1,104 @@
export const string = `[Unit]
Description=maia color scheme for the console
After=getty@%i.service
[Service]
Type=oneshot
ExecStart=/usr/bin/maia-console
StandardOutput=tty
TTYPath=/dev/%i
TTYVTDisallocate=yes
[Install]
WantedBy=getty@%i.service`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "maia color scheme for the console"
},
{
"type": "setting",
"name": "After",
"value": "getty@%i.service"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "oneshot"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/bin/maia-console"
},
{
"type": "setting",
"name": "StandardOutput",
"value": "tty"
},
{
"type": "setting",
"name": "TTYPath",
"value": "/dev/%i"
},
{
"type": "setting",
"name": "TTYVTDisallocate",
"value": "yes"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "WantedBy",
"value": "getty@%i.service"
}
]
}
]
};
export const data = [
{
title: 'Unit',
settings: {
Description: 'maia color scheme for the console',
After: 'getty@%i.service',
},
},
{
title: 'Service',
settings: {
Type: 'oneshot',
ExecStart: '/usr/bin/maia-console',
StandardOutput: 'tty',
TTYPath: '/dev/%i',
TTYVTDisallocate: 'yes',
},
},
{
title: 'Install',
settings: {
WantedBy: 'getty@%i.service',
},
},
];

View file

@ -0,0 +1,99 @@
export const string = `[Unit]
Description=Nix Daemon
RequiresMountsFor=/nix/store
RequiresMountsFor=/nix/var
ConditionPathIsReadWrite=/nix/var/nix/daemon-socket
[Service]
ExecStart=@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon
KillMode=process
[Install]
WantedBy=multi-user.target`;
export const ast = {
"type": "unitFile",
"comments": [],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "Nix Daemon"
},
{
"type": "setting",
"name": "RequiresMountsFor",
"value": "/nix/store"
},
{
"type": "setting",
"name": "RequiresMountsFor",
"value": "/nix/var"
},
{
"type": "setting",
"name": "ConditionPathIsReadWrite",
"value": "/nix/var/nix/daemon-socket"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "ExecStart",
"value": "@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon"
},
{
"type": "setting",
"name": "KillMode",
"value": "process"
}
]
},
{
"type": "section",
"title": "Install",
"body": [
{
"type": "setting",
"name": "WantedBy",
"value": "multi-user.target"
}
]
}
]
};
export const data = [
{
title: 'Unit',
settings: {
Description: 'Nix Daemon',
RequiresMountsFor: [
'/nix/store',
'/nix/var',
],
ConditionPathIsReadWrite: '/nix/var/nix/daemon-socket',
},
},
{
title: 'Service',
settings: {
ExecStart: '@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon',
KillMode: 'process',
},
},
{
title: 'Install',
settings: {
WantedBy: 'multi-user.target',
},
},
];

View file

@ -0,0 +1,130 @@
export const string = `# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=File System Check on Root Device
Documentation=man:systemd-fsck-root.service(8)
DefaultDependencies=no
Before=local-fs.target shutdown.target
ConditionPathIsReadWrite=!/
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-fsck
StandardOutput=null
StandardError=journal+console
TimeoutSec=0`;
export const ast = {
"type": "unitFile",
"comments": [
{
"type": "comment",
"value": " SPDX-License-Identifier: LGPL-2.1+"
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": " This file is part of systemd."
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": " systemd is free software; you can redistribute it and/or modify it"
},
{
"type": "comment",
"value": " under the terms of the GNU Lesser General Public License as published by"
},
{
"type": "comment",
"value": " the Free Software Foundation; either version 2.1 of the License, or"
},
{
"type": "comment",
"value": " (at your option) any later version."
}
],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "File System Check on Root Device"
},
{
"type": "setting",
"name": "Documentation",
"value": "man:systemd-fsck-root.service(8)"
},
{
"type": "setting",
"name": "DefaultDependencies",
"value": "no"
},
{
"type": "setting",
"name": "Before",
"value": "local-fs.target shutdown.target"
},
{
"type": "setting",
"name": "ConditionPathIsReadWrite",
"value": "!/"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "oneshot"
},
{
"type": "setting",
"name": "RemainAfterExit",
"value": "yes"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/lib/systemd/systemd-fsck"
},
{
"type": "setting",
"name": "StandardOutput",
"value": "null"
},
{
"type": "setting",
"name": "StandardError",
"value": "journal+console"
},
{
"type": "setting",
"name": "TimeoutSec",
"value": "0"
}
]
}
]
};

View file

@ -0,0 +1,136 @@
export const string = `# SPDX-License-Identifier: LGPL-2.1+
#
# This file is part of systemd.
#
# systemd is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
[Unit]
Description=File System Check on %f
Documentation=man:systemd-fsck@.service(8)
DefaultDependencies=no
BindsTo=%i.device
After=%i.device systemd-fsck-silent-root.service local-fs-pre.target
Before=systemd-quotacheck.service shutdown.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/lib/systemd/systemd-fsck
StandardOutput=null
StandardError=journal+console
TimeoutSec=0`;
export const ast = {
"type": "unitFile",
"comments": [
{
"type": "comment",
"value": " SPDX-License-Identifier: LGPL-2.1+"
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": " This file is part of systemd."
},
{
"type": "comment",
"value": ""
},
{
"type": "comment",
"value": " systemd is free software; you can redistribute it and/or modify it"
},
{
"type": "comment",
"value": " under the terms of the GNU Lesser General Public License as published by"
},
{
"type": "comment",
"value": " the Free Software Foundation; either version 2.1 of the License, or"
},
{
"type": "comment",
"value": " (at your option) any later version."
}
],
"sections": [
{
"type": "section",
"title": "Unit",
"body": [
{
"type": "setting",
"name": "Description",
"value": "File System Check on %f"
},
{
"type": "setting",
"name": "Documentation",
"value": "man:systemd-fsck@.service(8)"
},
{
"type": "setting",
"name": "DefaultDependencies",
"value": "no"
},
{
"type": "setting",
"name": "BindsTo",
"value": "%i.device"
},
{
"type": "setting",
"name": "After",
"value": "%i.device systemd-fsck-silent-root.service local-fs-pre.target"
},
{
"type": "setting",
"name": "Before",
"value": "systemd-quotacheck.service shutdown.target"
}
]
},
{
"type": "section",
"title": "Service",
"body": [
{
"type": "setting",
"name": "Type",
"value": "oneshot"
},
{
"type": "setting",
"name": "RemainAfterExit",
"value": "yes"
},
{
"type": "setting",
"name": "ExecStart",
"value": "/usr/lib/systemd/systemd-fsck"
},
{
"type": "setting",
"name": "StandardOutput",
"value": "null"
},
{
"type": "setting",
"name": "StandardError",
"value": "journal+console"
},
{
"type": "setting",
"name": "TimeoutSec",
"value": "0"
}
]
}
]
};

View file

@ -1,9 +1,9 @@
export const content = `[Install]
export const string = `[Install]
After=something.service
After=something-else.service
After=something-different.service`;
export const result = {
export const ast = {
type: 'unitFile',
comments: [],
sections: [
@ -34,3 +34,16 @@ export const result = {
}
]
};
export const data = [
{
title: 'Install',
settings: {
After: [
'something.service',
'something-else.service',
'something-different.service',
],
},
}
];

View file

@ -1,7 +1,7 @@
export const content = `[Install]
export const string = `[Install]
Description=Test description(1) https://something/? \\# # test`;
export const result = {
export const ast = {
type: 'unitFile',
comments: [],
sections: [
@ -20,3 +20,12 @@ export const result = {
}
]
};
export const data = [
{
title: 'Install',
settings: {
Description: 'Test description(1) https://something/? \\#',
},
}
];

43
src/spec/string-to-ast.mjs Executable file
View file

@ -0,0 +1,43 @@
import stringToAst from "../string-to-ast.mjs";
function runTest(input, result) {
const ast = stringToAst(input);
const resultString = JSON.stringify(ast, null, 2);
const expectedString = JSON.stringify(result, null, 2);
if (resultString !== expectedString) {
throw new Error(`mismatching result on testcase:
${input}
=======result======
${resultString}
=======expected======
${expectedString}
`);
}
};
(async () => {
const cases = await Promise.all([
"./parser/basic.mjs",
"./parser/comments.mjs",
"./parser/comment-only.mjs",
"./parser/multiline-value.mjs",
"./parser/weird-characters.mjs",
"./parser/repeated-settings.mjs",
"./parser/real.nix-daemon.mjs",
"./parser/real.maia-console@.mjs",
"./parser/real.dbus-org.bluez.mjs",
"./parser/real.display-manager.mjs",
"./parser/real.systemd-fsck-silent@.mjs",
"./parser/real.systemd-fsck-silent-root.mjs",
"./parser/real.dbus-org.freedesktop.Avahi.mjs",
"./parser/real.dbus-org.freedesktop.ModemManager1.mjs",
"./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs",
].map(file => import(file)));
const results = cases.forEach(({ string, ast }) => runTest(string, ast));
})();

30
src/string-to-ast.mjs Executable file
View file

@ -0,0 +1,30 @@
import Parser from "./Parser.mjs";
import Lexer from "./Lexer.mjs";
import visitorCreator from "./visitor-creator.mjs";
export default function stringToAst(input, c) {
const config = c || { strict: true };
const parser = new Parser([], { outputCst: true });
const lexingresult = Lexer.tokenize(input);
if (config.strict && lexingresult.errors.length > 0) {
console.log(input);
console.dir(lexingresult, { depth: Infinity });
throw new Error("Lexing errors detected")
}
parser.input = lexingresult.tokens;
const cst = parser.unitFile();
if (config.strict && parser.errors.length > 0) {
console.log(input);
//console.dir(lexingresult, { depth: Infinity });
console.dir(parser.errors, { depth: Infinity });
throw new Error("Parsing errors detected")
}
const Visitor = visitorCreator(parser);
const visitor = new Visitor();
const ast = visitor.visit(cst);
return ast;
}

6
src/string-to-data.mjs Executable file
View file

@ -0,0 +1,6 @@
import stringToAst from "./string-to-ast.mjs";
import astToData from "./astToData.mjs";
export default function stringToData(data) {
return astToData(stringToAst(data));
}

12
src/visitor.mjs → src/visitor-creator.mjs Normal file → Executable file
View file

@ -1,4 +1,4 @@
export default (parser) => {
export default function visitorCreator(parser) {
const BaseSQLVisitorWithDefaults = parser.getBaseCstVisitorConstructorWithDefaults();
return class UnitFileVisitor extends BaseSQLVisitorWithDefaults {
@ -10,7 +10,7 @@ export default (parser) => {
_comment(ctx) {
return {
type: "comment",
value: ctx.Comment.map(({ image }) => image.replace(/^\s/, '').replace(/\s$/, '')).join(''),
value: ctx.Comment.map(({ image }) => image.replace(/^#\s?/, '').replace(/\s$/, '')).join(''),
};
}
@ -18,10 +18,6 @@ export default (parser) => {
return this._comment(ctx);
}
commentLine(ctx) {
return this._comment(ctx);
}
propertyStatement(ctx) {
const name = ctx.Property[0].image;
return {
@ -33,7 +29,7 @@ export default (parser) => {
}
sectionStatement(ctx) {
const content = ctx.propertyStatement?.[0] || ctx.commentLine?.[0];
const content = ctx.propertyStatement?.[0] || ctx.comment?.[0];
return this.visit(content);
}
@ -58,4 +54,4 @@ export default (parser) => {
};
}
};
};
}

View file

@ -1,7 +0,0 @@
export const content = `# Start of file comment`;
export const result = {
type: 'unitFile',
comments: [ { type: 'comment', value: 'Start of file comment' } ],
sections: []
};

View file

@ -1,67 +0,0 @@
import glob from "glob";
import { join } from "path";
import {
Lexer,
Parser,
visitorCreator,
} from "../src/mod.mjs";
const runTest = (input, result) => {
const parser = new Parser([], { outputCst: true });
const lexingresult = Lexer.tokenize(input);
if (lexingresult.errors.length > 0) {
console.dir(lexingresult, { depth: Infinity });
throw new Error("Lexing errors detected")
}
parser.input = lexingresult.tokens;
const cst = parser.unitFile();
if (parser.errors.length > 0) {
console.dir(parser.errors, { depth: Infinity });
throw new Error("Parsing errors detected")
}
const Visitor = visitorCreator(parser);
const visitor = new Visitor();
const ast = visitor.visit(cst);
const resultString = JSON.stringify(ast, null, 2);
const expectedString = JSON.stringify(result, null, 2);
if (resultString !== expectedString) {
console.dir(ast, { depth: Infinity });
console.dir(lexingresult, { depth: Infinity });
throw new Error(`Mismatching result on testcase:
${input}
=======Result======
${resultString}
=======Expected======
${expectedString}
`);
}
};
(async () => {
const files = await Promise.all(
process.argv
.slice(2)
.map(arg => new Promise(
(resolve, reject) => glob(arg, {}, (err, files) => {
if (err) {
reject(err);
} else {
resolve(files.map(file => join(process.cwd(), file)));
}
})
))
);
const cases = await Promise.all(files.flat().map(file => import(file)));
const results = cases.forEach(({ content, result }) => runTest(content, result));
})();