Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,9 @@
[submodule "vendor/grammars/typst-grammar"]
path = vendor/grammars/typst-grammar
url = https://github.com/michidk/typst-grammar.git
[submodule "vendor/grammars/ucode-lsp"]
path = vendor/grammars/ucode-lsp
url = https://github.com/NoahBPeterson/ucode-lsp.git
[submodule "vendor/grammars/verilog.tmbundle"]
path = vendor/grammars/verilog.tmbundle
url = https://github.com/textmate/verilog.tmbundle
Expand Down
2 changes: 2 additions & 0 deletions grammars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,8 @@ vendor/grammars/typespec:
- source.tsp
vendor/grammars/typst-grammar:
- source.typst
vendor/grammars/ucode-lsp:
- source.ucode
vendor/grammars/verilog.tmbundle:
- source.verilog
vendor/grammars/vsc-ember-syntax:
Expand Down
19 changes: 19 additions & 0 deletions lib/linguist/heuristics.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,12 @@ disambiguations:
- language: Typst
pattern: '^#(import|show|let|set)'
- language: XML
- extensions: ['.uc']
rules:
- language: ucode
named_pattern: ucode
- language: UnrealScript
named_pattern: unrealscript
- extensions: ['.url']
rules:
- language: INI
Expand Down Expand Up @@ -1137,6 +1143,19 @@ named_patterns:
- '(?i)^[ ]*\$(CONSOLE|CHECKING):'
- '(?i)^[ ]*\$(FULLSCREEN|RESIZE|STATIC|DYNAMIC|NOPREFIX|SCREENSHOW|SCREENHIDE|EXEICON)\b'
raku: '^\s*(?:use\s+v6\b|\bmodule\b|\b(?:my\s+)?class\b)'
ucode:
- '^#!.*\bucode\b'
- '^\s*[''"]use strict[''"];'
- '^[ \t]*(?:export|import)\b'
- '\b(?:require|include)\s*\(\s*[''"]'
- '^\s*let\s+\w+'
- '\b(?:printf|sprintf)\s*\('
unrealscript:
- '(?i)^\s*class\s+\w+\s+extends\s+[\w.]+'
- '(?i)^\s*defaultproperties\b'
- '(?i)^\s*var\s*\('
- '(?i)^\s*(?:simulated|exec|native|reliable|unreliable|static\s+final|final)\s+(?:function|event)\b'
- '(?i)^\s*#exec\b'
vb-class: '^[ ]*VERSION [0-9]\.[0-9] CLASS'
vb-form: '^[ ]*VERSION [0-9]\.[0-9]{2}'
vb-module: '^[ ]*Attribute VB_Name = '
Expand Down
12 changes: 12 additions & 0 deletions lib/linguist/languages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9413,6 +9413,18 @@ templ:
ace_mode: text
tm_scope: source.templ
language_id: 795579337
ucode:
type: programming
color: "#00b8d4"
extensions:
- ".uc"
interpreters:
- ucode
ace_mode: javascript
codemirror_mode: javascript
codemirror_mime_type: text/javascript
tm_scope: source.ucode
language_id: 365454253
vCard:
type: data
color: "#ee2647"
Expand Down
256 changes: 256 additions & 0 deletions samples/ucode/commands.uc
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright 2012-2022 Jo-Philipp Wich <jow@openwrt.org>
// Licensed to the public under the Apache License 2.0.

'use strict';

import { basename, mkstemp, popen } from 'fs';
import { urldecode } from 'luci.http';

// Decode a given string into arguments following shell quoting rules
// [[abc\ def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
function parse_args(str) {
let args = [];

function isspace(c) {
if (c == 9 || c == 10 || c == 11 || c == 12 || c == 13 || c == 32)
return c;
}

function isquote(c) {
if (c == 34 || c == 39 || c == 96)
return c;
}

function isescape(c) {
if (c == 92)
return c;
}

function ismeta(c) {
if (c == 36 || c == 92 || c == 96)
return c;
}

// Scan substring defined by the indexes [s, e] of the string "str",
// perform unquoting and de-escaping on the fly and store the result
function unquote(start, end) {
let esc, quote, res = [];

for (let off = start; off < end; off++) {
const byte = ord(str, off);
const q = isquote(byte);
const e = isescape(byte);
const m = ismeta(byte);

if (esc) {
if (!m)
push(res, 92);

push(res, byte);
esc = false;
}
else if (e && quote != 39) {
esc = true;
}
else if (q && quote && q == quote) {
quote = null;
}
else if (q && !quote) {
quote = q;
}
else {
push(res, byte);
}
}

push(args, chr(...res));
}

// Find substring boundaries in "str". Ignore escaped or quoted
// whitespace, pass found start- and end-index for each substring
// to unquote()
let esc, start, quote;

for (let off = 0; off <= length(str); off++) {
const byte = ord(str, off);
const q = isquote(byte);
const s = isspace(byte) ?? (byte === null);
const e = isescape(byte);

if (esc) {
esc = false;
}
else if (e && quote != 39) {
esc = true;
start ??= off;
}
else if (q && quote && q == quote) {
quote = null;
}
else if (q && !quote) {
start ??= off;
quote = q;
}
else if (s && !quote) {
if (start !== null) {
unquote(start, off);
start = null;
}
}
else {
start ??= off;
}
}

// If the "quote" is still set we encountered an unfinished string
if (quote)
unquote(start, length(str));

return args;
}

function test_binary(str) {
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
if (byte <= 8 || (byte >= 14 && byte <= 31))
return true;

return false;
}

function parse_cmdline(cmdid, args) {
if (uci.get('luci', cmdid) == 'command') {
let cmd = uci.get_all('luci', cmdid);
let argv = parse_args(cmd?.command);

if (cmd?.param == '1') {
if (length(args))
push(argv, ...(parse_args(urldecode(args)) ?? []));
else if (length(args = http.formvalue('args')))
push(argv, ...(parse_args(args) ?? []));
}

return map(argv, v => match(v, /[^\w.\/|-]/) ? `'${replace(v, "'", "'\\''")}'` : v);
}
}

function execute_command(callback, ...args) {
let argv = parse_cmdline(...args);

if (argv) {
let outfd = mkstemp();
let errfd = mkstemp();

const exitcode = system(`${join(' ', argv)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);

outfd.seek(0);
errfd.seek(0);

const stdout = outfd.read(1024 * 512) ?? '';
const stderr = errfd.read(1024 * 512) ?? '';

outfd.close();
errfd.close();

const binary = test_binary(stdout);

callback({
ok: true,
command: join(' ', argv),
stdout: binary ? null : stdout,
stderr,
exitcode,
binary
});
}
else {
callback({
ok: false,
code: 404,
reason: "No such command"
});
}
}

function return_json(result) {
if (result.ok) {
http.prepare_content('application/json');
http.write_json(result);
}
else {
http.status(result.code, result.reason);
}
}


function return_html(result) {
if (result.ok) {
include('commands_public', result);
}
else {
http.status(result.code, result.reason);
}
}

return {
action_run: function(...args) {
execute_command(return_json, ...args);
},

action_download: function(...args) {
const argv = parse_cmdline(...args);

if (argv) {
const fd = popen(`${join(' ', argv)} 2>/dev/null`);

if (fd) {
let filename = replace(basename(argv[0]), /\W+/g, '.');
let chunk = fd.read(4096) ?? '';
let name;

if (test_binary(chunk)) {
http.header("Content-Disposition", `attachment; filename=${filename}.bin`);
http.prepare_content("application/octet-stream");
}
else {
http.header("Content-Disposition", `attachment; filename=${filename}.txt`);
http.prepare_content("text/plain");
}

while (length(chunk)) {
http.write(chunk);
chunk = fd.read(4096);
}

fd.close();
}
else {
http.status(500, "Failed to execute command");
}
}
else {
http.status(404, "No such command");
}
},

action_public: function(cmdid, ...args) {
let disp = false;

if (substr(cmdid, -1) == "s") {
disp = true;
cmdid = substr(cmdid, 0, -1);
}

if (cmdid &&
uci.get('luci', cmdid) == 'command' &&
uci.get('luci', cmdid, 'public') == '1')
{
if (disp)
execute_command(return_html, cmdid, ...args);
else
this.action_download(cmdid, args);
}
else {
http.status(403, "Access to command denied");
}
}
};
Loading