mirror of
https://github.com/arendst/Tasmota.git
synced 2025-07-22 10:16:30 +00:00
Berry added f-strings as an alternative to string formatting (#18937)
This commit is contained in:
parent
558f812ec2
commit
85f357096a
@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
- Berry `string.format()` now automatically converts type according to format
|
- Berry `string.format()` now automatically converts type according to format
|
||||||
- Matter add friendly-name (NodeLabel) to each endpoint
|
- Matter add friendly-name (NodeLabel) to each endpoint
|
||||||
- Berry add global function `format` as a simpler syntax to `string.format`
|
- Berry add global function `format` as a simpler syntax to `string.format`
|
||||||
|
- Berry added f-strings as an alternative to string formatting
|
||||||
|
|
||||||
### Breaking Changed
|
### Breaking Changed
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ void be_print_inst(binstruction ins, int pc, void* fout)
|
|||||||
logbuf("%s", opc2str(op));
|
logbuf("%s", opc2str(op));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
memcpy(__lbuf_tmp, __lbuf, strlen(__lbuf)+1);
|
memcpy(__lbuf_tmp, __lbuf, strlen(__lbuf) + 1);
|
||||||
logbuf("%s\n", __lbuf_tmp);
|
logbuf("%s\n", __lbuf_tmp);
|
||||||
if (fout) {
|
if (fout) {
|
||||||
be_fwrite(fout, __lbuf, strlen(__lbuf));
|
be_fwrite(fout, __lbuf, strlen(__lbuf));
|
||||||
@ -184,7 +184,7 @@ static void sourceinfo(bproto *proto, binstruction *ip)
|
|||||||
blineinfo *end = it + proto->nlineinfo;
|
blineinfo *end = it + proto->nlineinfo;
|
||||||
int pc = cast_int(ip - proto->code - 1); /* now vm->ip has been increased */
|
int pc = cast_int(ip - proto->code - 1); /* now vm->ip has been increased */
|
||||||
for (; it < end && pc > it->endpc; ++it);
|
for (; it < end && pc > it->endpc; ++it);
|
||||||
sprintf(buf, ":%d:", it->linenumber);
|
snprintf(buf, sizeof(buf), ":%d:", it->linenumber);
|
||||||
be_writestring(str(proto->source));
|
be_writestring(str(proto->source));
|
||||||
be_writestring(buf);
|
be_writestring(buf);
|
||||||
} else {
|
} else {
|
||||||
|
@ -183,8 +183,9 @@ int be_protectedparser(bvm *vm,
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* _sgets(void *data, size_t *size)
|
static const char* _sgets(struct blexer* lexer, void *data, size_t *size)
|
||||||
{
|
{
|
||||||
|
(void)lexer;
|
||||||
struct strbuf *sb = data;
|
struct strbuf *sb = data;
|
||||||
*size = sb->len;
|
*size = sb->len;
|
||||||
if (sb->len) {
|
if (sb->len) {
|
||||||
@ -194,8 +195,9 @@ static const char* _sgets(void *data, size_t *size)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char* _fgets(void *data, size_t *size)
|
static const char* _fgets(struct blexer* lexer, void *data, size_t *size)
|
||||||
{
|
{
|
||||||
|
(void)lexer;
|
||||||
struct filebuf *fb = data;
|
struct filebuf *fb = data;
|
||||||
*size = be_fread(fb->fp, fb->buf, sizeof(fb->buf));
|
*size = be_fread(fb->fp, fb->buf, sizeof(fb->buf));
|
||||||
if (*size) {
|
if (*size) {
|
||||||
|
@ -13,14 +13,15 @@
|
|||||||
#include "be_map.h"
|
#include "be_map.h"
|
||||||
#include "be_vm.h"
|
#include "be_vm.h"
|
||||||
#include "be_strlib.h"
|
#include "be_strlib.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
#define SHORT_STR_LEN 32
|
#define SHORT_STR_LEN 32
|
||||||
#define EOS '\0' /* end of source */
|
#define EOS '\0' /* end of source */
|
||||||
|
|
||||||
#define type_count() (int)array_count(kwords_tab)
|
#define type_count() (int)array_count(kwords_tab)
|
||||||
#define lexbuf(lex) ((lex)->buf.s)
|
#define lexbuf(lex) ((lex)->buf.s)
|
||||||
#define isvalid(lex) ((lex)->cursor < (lex)->endbuf)
|
#define isvalid(lex) ((lex)->reader.cursor < (lex)->endbuf)
|
||||||
#define lgetc(lex) ((lex)->cursor)
|
#define lgetc(lex) ((lex)->reader.cursor)
|
||||||
#define setstr(lex, v) ((lex)->token.u.s = (v))
|
#define setstr(lex, v) ((lex)->token.u.s = (v))
|
||||||
#define setint(lex, v) ((lex)->token.u.i = (v))
|
#define setint(lex, v) ((lex)->token.u.i = (v))
|
||||||
#define setreal(lex, v) ((lex)->token.u.r = (v))
|
#define setreal(lex, v) ((lex)->token.u.r = (v))
|
||||||
@ -39,7 +40,8 @@ static const char* const kwords_tab[] = {
|
|||||||
":", "?", "->", "if", "elif", "else", "while",
|
":", "?", "->", "if", "elif", "else", "while",
|
||||||
"for", "def", "end", "class", "break", "continue",
|
"for", "def", "end", "class", "break", "continue",
|
||||||
"return", "true", "false", "nil", "var", "do",
|
"return", "true", "false", "nil", "var", "do",
|
||||||
"import", "as", "try", "except", "raise", "static"
|
"import", "as", "try", "except", "raise", "static",
|
||||||
|
// ".f"
|
||||||
};
|
};
|
||||||
|
|
||||||
void be_lexerror(blexer *lexer, const char *msg)
|
void be_lexerror(blexer *lexer, const char *msg)
|
||||||
@ -102,12 +104,12 @@ static int next(blexer *lexer)
|
|||||||
struct blexerreader *lr = &lexer->reader;
|
struct blexerreader *lr = &lexer->reader;
|
||||||
if (!(lr->len--)) {
|
if (!(lr->len--)) {
|
||||||
static const char eos = EOS;
|
static const char eos = EOS;
|
||||||
const char *s = lr->readf(lr->data, &lr->len);
|
const char *s = lr->readf(lexer, lr->data, &lr->len);
|
||||||
lr->s = s ? s : &eos;
|
lr->s = s ? s : &eos;
|
||||||
--lr->len;
|
--lr->len;
|
||||||
}
|
}
|
||||||
lexer->cursor = *lr->s++;
|
lexer->reader.cursor = *lr->s++;
|
||||||
return lexer->cursor;
|
return lexer->reader.cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_buf(blexer *lexer)
|
static void clear_buf(blexer *lexer)
|
||||||
@ -115,10 +117,7 @@ static void clear_buf(blexer *lexer)
|
|||||||
lexer->buf.len = 0;
|
lexer->buf.len = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* save and next */
|
static void save_char(blexer *lexer, int ch) {
|
||||||
static int save(blexer *lexer)
|
|
||||||
{
|
|
||||||
int ch = lgetc(lexer);
|
|
||||||
struct blexerbuf *buf = &lexer->buf;
|
struct blexerbuf *buf = &lexer->buf;
|
||||||
if (buf->len >= buf->size) {
|
if (buf->len >= buf->size) {
|
||||||
size_t size = buf->size << 1;
|
size_t size = buf->size << 1;
|
||||||
@ -126,6 +125,13 @@ static int save(blexer *lexer)
|
|||||||
buf->size = size;
|
buf->size = size;
|
||||||
}
|
}
|
||||||
buf->s[buf->len++] = (char)ch;
|
buf->s[buf->len++] = (char)ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* save and next */
|
||||||
|
static int save(blexer *lexer)
|
||||||
|
{
|
||||||
|
int ch = lgetc(lexer);
|
||||||
|
save_char(lexer, ch);
|
||||||
return next(lexer);
|
return next(lexer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -247,7 +253,7 @@ static int skip_newline(blexer *lexer)
|
|||||||
next(lexer); /* skip "\n\r" or "\r\n" */
|
next(lexer); /* skip "\n\r" or "\r\n" */
|
||||||
}
|
}
|
||||||
lexer->linenumber++;
|
lexer->linenumber++;
|
||||||
return lexer->cursor;
|
return lexer->reader.cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void skip_comment(blexer *lexer)
|
static void skip_comment(blexer *lexer)
|
||||||
@ -373,12 +379,217 @@ static btokentype scan_numeral(blexer *lexer)
|
|||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* structure for a temporary reader used by transpiler, with attributes for an allocated buffer and size */
|
||||||
|
struct blexerreader_save {
|
||||||
|
struct blexerreader reader;
|
||||||
|
char* s;
|
||||||
|
size_t size;
|
||||||
|
char cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* buf reader for transpiled code from f-strings */
|
||||||
|
/* it restores the original reader when the transpiler buffer is empty */
|
||||||
|
/* the first pass returns a single byte buffer with the saved cursor */
|
||||||
|
/* the second pass restores the original reader */
|
||||||
|
static const char* _bufgets(struct blexer* lexer, void *data, size_t *size)
|
||||||
|
{
|
||||||
|
/* this is called once the temporaty transpiler buffer is empty */
|
||||||
|
struct blexerreader *reader = &lexer->reader; /* current reader which is temporary only for the transpiler */
|
||||||
|
struct blexerreader_save *reader_save = data; /* original reader that needs to be restored once the buffer is empty */
|
||||||
|
|
||||||
|
/* first case, we saved the cursor (fist char), we server it now */
|
||||||
|
if (reader_save->reader.cursor >= 0) { /* serve the previously saved cursor */
|
||||||
|
/* copy cursor to a 'char' type */
|
||||||
|
reader_save->cursor = reader_save->reader.cursor;
|
||||||
|
reader_save->reader.cursor = -1; /* no more cursor saved */
|
||||||
|
*size = 1;
|
||||||
|
return &reader_save->cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* second case, the saved cursor was returned, now restore the normal flow of the original reader */
|
||||||
|
/* restore the original reader */
|
||||||
|
*reader = reader_save->reader;
|
||||||
|
|
||||||
|
/* free the memory from the structure */
|
||||||
|
be_free(lexer->vm, reader_save->s, reader_save->size); /* free the buffer */
|
||||||
|
be_free(lexer->vm, reader_save, sizeof(struct blexerreader_save)); /* free the structure */
|
||||||
|
|
||||||
|
if (!reader->len) { /* just in case the original buffer was also */
|
||||||
|
return reader->readf(lexer, reader->data, size);
|
||||||
|
}
|
||||||
|
/* the following is not necessary, but safer */
|
||||||
|
*size = reader->len;
|
||||||
|
return reader->s;
|
||||||
|
}
|
||||||
|
|
||||||
|
static btokentype scan_string(blexer *lexer); /* forward declaration */
|
||||||
|
|
||||||
|
/* scan f-string and transpile it to `format(...)` syntax then feeding the normal lexer and parser */
|
||||||
|
static void scan_f_string(blexer *lexer)
|
||||||
|
{
|
||||||
|
char ch;
|
||||||
|
clear_buf(lexer);
|
||||||
|
scan_string(lexer); /* first scan the entire string in lexer->buf */
|
||||||
|
|
||||||
|
/* save original reader until the transpiled is processed */
|
||||||
|
/* reader will be restored by the reader function once the transpiled buffer is empty */
|
||||||
|
struct blexerreader_save *reader_save = (struct blexerreader_save *) be_malloc(lexer->vm, sizeof(struct blexerreader_save)); /* temporary reader */
|
||||||
|
reader_save->reader = lexer->reader;
|
||||||
|
|
||||||
|
/* save blexerbuf which contains the unparsed_fstring */
|
||||||
|
struct blexerbuf buf_unparsed_fstr = lexer->buf;
|
||||||
|
|
||||||
|
/* prepare and allocated a temporary buffer to save parsed f_string */
|
||||||
|
lexer->buf.size = lexer->buf.size + 20;
|
||||||
|
lexer->buf.s = be_malloc(lexer->vm, lexer->buf.size);
|
||||||
|
lexer->buf.len = 0;
|
||||||
|
|
||||||
|
/* parse f_string */
|
||||||
|
/* First pass, check syntax and extract string literals, and format */
|
||||||
|
save_char(lexer, '(');
|
||||||
|
save_char(lexer, '"');
|
||||||
|
for (size_t i = 0; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
switch (ch) {
|
||||||
|
case '%': /* % needs to be encoded as %% */
|
||||||
|
save_char(lexer, '%');
|
||||||
|
save_char(lexer, '%');
|
||||||
|
break;
|
||||||
|
case '\\': /* \ needs to be encoded as \\ */
|
||||||
|
save_char(lexer, '\\');
|
||||||
|
save_char(lexer, '\\');
|
||||||
|
break;
|
||||||
|
case '"': /* " needs to be encoded as \" */
|
||||||
|
save_char(lexer, '\\');
|
||||||
|
save_char(lexer, '"');
|
||||||
|
break;
|
||||||
|
case '}': /* }} is converted as } yet we tolerate a single } */
|
||||||
|
if ((i+1 < buf_unparsed_fstr.len) && (buf_unparsed_fstr.s[i+1] == '}')) { i++; } /* if '}}' replace with '}' */
|
||||||
|
save_char(lexer, '}');
|
||||||
|
break;
|
||||||
|
default: /* copy any other character */
|
||||||
|
save_char(lexer, ch);
|
||||||
|
break;
|
||||||
|
case '{': /* special case for { */
|
||||||
|
i++; /* in all cases skip to next char */
|
||||||
|
if ((i < buf_unparsed_fstr.len) && (buf_unparsed_fstr.s[i] == '{')) {
|
||||||
|
save_char(lexer, '{'); /* {{ is simply encoded as { and continue parsing */
|
||||||
|
} else {
|
||||||
|
/* we still don't know if '=' is present, so we copy the expression each time, and rollback if we find out the '=' is not present */
|
||||||
|
size_t rollback = lexer->buf.len; /* mark the end of string for later rollback if '=' is not present */
|
||||||
|
/* parse inner part */
|
||||||
|
/* skip everything until either ':' or '}' or '=' */
|
||||||
|
/* if end of string is reached before '}' raise en error */
|
||||||
|
for (; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
if (ch == ':' || ch == '}') { break; }
|
||||||
|
save_char(lexer, ch); /* copy any character unless it's ':' or '}' */
|
||||||
|
if (ch == '=') { break; } /* '=' is copied but breaks parsing as well */
|
||||||
|
}
|
||||||
|
/* safe check if we reached the end of the string */
|
||||||
|
if (i >= buf_unparsed_fstr.len) { be_raise(lexer->vm, "syntax_error", "'}' expected"); }
|
||||||
|
/* if '=' detected then do some additional checks */
|
||||||
|
if (ch == '=') {
|
||||||
|
i++; /* skip '=' and check we haven't reached the end */
|
||||||
|
if (i >= buf_unparsed_fstr.len) { be_raise(lexer->vm, "syntax_error", "'}' expected"); }
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
if ((ch != ':') && (ch != '}')) { /* '=' must be immediately followed by ':' or '}' */
|
||||||
|
be_raise(lexer->vm, "syntax_error", "':' or '}' expected after '='");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/* no '=' present, rollback the text of the expression */
|
||||||
|
lexer->buf.len = rollback;
|
||||||
|
}
|
||||||
|
save_char(lexer, '%'); /* start format encoding */
|
||||||
|
if (ch == ':') {
|
||||||
|
/* copy format */
|
||||||
|
i++;
|
||||||
|
if ((i < buf_unparsed_fstr.len) && (buf_unparsed_fstr.s[i] == '%')) { i++; } /* skip '%' following ':' */
|
||||||
|
for (; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
if (ch == '}') { break; }
|
||||||
|
save_char(lexer, ch);
|
||||||
|
}
|
||||||
|
if (i >= buf_unparsed_fstr.len) { be_raise(lexer->vm, "syntax_error", "'}' expected"); }
|
||||||
|
} else {
|
||||||
|
/* if no formatting, output '%s' */
|
||||||
|
save_char(lexer, 's');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save_char(lexer, '"'); /* finish format string */
|
||||||
|
|
||||||
|
/* Second pass - add arguments if any */
|
||||||
|
for (size_t i = 0; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
/* skip any character that is not '{' followed by '{' */
|
||||||
|
if (buf_unparsed_fstr.s[i] == '{') {
|
||||||
|
i++; /* in all cases skip to next char */
|
||||||
|
if ((i < buf_unparsed_fstr.len) && (buf_unparsed_fstr.s[i] == '{')) { continue; }
|
||||||
|
/* extract argument */
|
||||||
|
save_char(lexer, ','); /* add ',' to start next argument to `format()` */
|
||||||
|
for (; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
if (ch == '=' || ch == ':' || ch == '}') { break; }
|
||||||
|
save_char(lexer, ch); /* copy expression until we reach ':', '=' or '}' */
|
||||||
|
}
|
||||||
|
/* no need to check for end of string here, it was done already in first pass */
|
||||||
|
if (ch == ':' || ch == '=') { /* if '=' or ':', skip everyting until '}' */
|
||||||
|
i++;
|
||||||
|
for (; i < buf_unparsed_fstr.len; i++) {
|
||||||
|
ch = buf_unparsed_fstr.s[i];
|
||||||
|
if (ch == '}') { break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
save_char(lexer, ')'); /* add final ')' */
|
||||||
|
|
||||||
|
/* Situation now: */
|
||||||
|
/* `buf_unparsed_fstr` contains the buffer of the input unparsed f-string, ex: "age: {age:2i}" */
|
||||||
|
/* `lexer->buf` contains the buffer of the transpiled f-string without call to format(), ex: '("age: %2i", age)' */
|
||||||
|
/* `reader_save` contains the original reader to continue parsing after f-string */
|
||||||
|
/* `lexer->reader` will contain a temporary reader from the parsed f-string */
|
||||||
|
|
||||||
|
/* extract the parsed f-string from the temporary buffer (needs later deallocation) */
|
||||||
|
char * parsed_fstr_s = lexer->buf.s; /* needs later deallocation with parsed_fstr_size */
|
||||||
|
size_t parsed_fstr_len = lexer->buf.len;
|
||||||
|
size_t parsed_fstr_size = lexer->buf.size;
|
||||||
|
|
||||||
|
/* restore buf to lexer */
|
||||||
|
lexer->buf = buf_unparsed_fstr;
|
||||||
|
|
||||||
|
/* change the temporary reader to the parsed f-string */
|
||||||
|
lexer->reader.len = parsed_fstr_len;
|
||||||
|
lexer->reader.data = (void*) reader_save; /* link to the saved context */
|
||||||
|
lexer->reader.s = parsed_fstr_s; /* reader is responisble to deallocate later this buffer */
|
||||||
|
lexer->reader.readf = _bufgets;
|
||||||
|
|
||||||
|
/* add information needed for `_bufgets` to later deallocate the buffer */
|
||||||
|
reader_save->size = parsed_fstr_size;
|
||||||
|
reader_save->s = parsed_fstr_s;
|
||||||
|
|
||||||
|
/* start parsing the parsed f-string, which is btw always '(' */
|
||||||
|
next(lexer);
|
||||||
|
|
||||||
|
/* remember that we are still in `scan_identifier()`, we replace the 'f' identifier to 'format' which is the global function to call */
|
||||||
|
static const char FORMAT[] = "format";
|
||||||
|
lexer->buf.len = sizeof(FORMAT) - 1; /* we now that buf size is at least SHORT_STR_LEN (32) */
|
||||||
|
memmove(lexer->buf.s, FORMAT, lexer->buf.len);
|
||||||
|
}
|
||||||
|
|
||||||
static btokentype scan_identifier(blexer *lexer)
|
static btokentype scan_identifier(blexer *lexer)
|
||||||
{
|
{
|
||||||
int type;
|
int type;
|
||||||
bstring *s;
|
bstring *s;
|
||||||
save(lexer);
|
save(lexer);
|
||||||
match(lexer, is_word);
|
match(lexer, is_word);
|
||||||
|
/* check if the form is f"aaaa" or f'aaa' */
|
||||||
|
char ch = lgetc(lexer);
|
||||||
|
if ((lexer->buf.len == 1) && (lexer->buf.s[0] == 'f') && (ch == '"' || ch == '\'')) {
|
||||||
|
scan_f_string(lexer);
|
||||||
|
}
|
||||||
s = buf_tostr(lexer);
|
s = buf_tostr(lexer);
|
||||||
type = str_extra(s);
|
type = str_extra(s);
|
||||||
if (type >= KeyIf && type < type_count()) {
|
if (type >= KeyIf && type < type_count()) {
|
||||||
|
@ -97,6 +97,7 @@ struct blexerreader {
|
|||||||
size_t len;
|
size_t len;
|
||||||
void *data;
|
void *data;
|
||||||
breader readf;
|
breader readf;
|
||||||
|
int cursor;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct blexerbuf {
|
struct blexerbuf {
|
||||||
@ -123,7 +124,6 @@ typedef struct blexer {
|
|||||||
struct blexerreader reader;
|
struct blexerreader reader;
|
||||||
bmap *strtab;
|
bmap *strtab;
|
||||||
bvm *vm;
|
bvm *vm;
|
||||||
int cursor;
|
|
||||||
} blexer;
|
} blexer;
|
||||||
|
|
||||||
void be_lexer_init(blexer *lexer, bvm *vm,
|
void be_lexer_init(blexer *lexer, bvm *vm,
|
||||||
|
@ -195,7 +195,8 @@ typedef struct {
|
|||||||
bntvfunc destroy;
|
bntvfunc destroy;
|
||||||
} bcommomobj;
|
} bcommomobj;
|
||||||
|
|
||||||
typedef const char* (*breader)(void*, size_t*);
|
struct blexer;
|
||||||
|
typedef const char* (*breader)(struct blexer*, void*, size_t*);
|
||||||
|
|
||||||
#define cast(_T, _v) ((_T)(_v))
|
#define cast(_T, _v) ((_T)(_v))
|
||||||
#define cast_int(_v) cast(int, _v)
|
#define cast_int(_v) cast(int, _v)
|
||||||
|
@ -52,22 +52,22 @@ bstring* be_num2str(bvm *vm, bvalue *v)
|
|||||||
{
|
{
|
||||||
char buf[25];
|
char buf[25];
|
||||||
if (var_isint(v)) {
|
if (var_isint(v)) {
|
||||||
sprintf(buf, BE_INT_FORMAT, var_toint(v));
|
snprintf(buf, sizeof(buf),BE_INT_FORMAT, var_toint(v));
|
||||||
} else if (var_isreal(v)) {
|
} else if (var_isreal(v)) {
|
||||||
sprintf(buf, "%g", var_toreal(v));
|
snprintf(buf, sizeof(buf), "%g", var_toreal(v));
|
||||||
} else {
|
} else {
|
||||||
sprintf(buf, "(nan)");
|
snprintf(buf, sizeof(buf), "(nan)");
|
||||||
}
|
}
|
||||||
return be_newstr(vm, buf);
|
return be_newstr(vm, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void module2str(char *buf, bvalue *v)
|
static void module2str(char *buf, size_t buf_len, bvalue *v)
|
||||||
{
|
{
|
||||||
const char *name = be_module_name(cast(bmodule*, var_toobj(v)));
|
const char *name = be_module_name(cast(bmodule*, var_toobj(v)));
|
||||||
if (name) {
|
if (name) {
|
||||||
sprintf(buf, "<module: %s>", name);
|
snprintf(buf, buf_len, "<module: %s>", name);
|
||||||
} else {
|
} else {
|
||||||
sprintf(buf, "<module: %p>", var_toobj(v));
|
snprintf(buf, buf_len, "<module: %p>", var_toobj(v));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,26 +83,26 @@ static bstring* sim2str(bvm *vm, bvalue *v)
|
|||||||
break;
|
break;
|
||||||
case BE_INDEX:
|
case BE_INDEX:
|
||||||
case BE_INT:
|
case BE_INT:
|
||||||
sprintf(sbuf, BE_INT_FORMAT, var_toint(v));
|
snprintf(sbuf, sizeof(sbuf), BE_INT_FORMAT, var_toint(v));
|
||||||
break;
|
break;
|
||||||
case BE_REAL:
|
case BE_REAL:
|
||||||
sprintf(sbuf, "%g", var_toreal(v));
|
snprintf(sbuf, sizeof(sbuf), "%g", var_toreal(v));
|
||||||
break;
|
break;
|
||||||
case BE_CLOSURE: case BE_NTVCLOS: case BE_NTVFUNC: case BE_CTYPE_FUNC:
|
case BE_CLOSURE: case BE_NTVCLOS: case BE_NTVFUNC: case BE_CTYPE_FUNC:
|
||||||
sprintf(sbuf, "<function: %p>", var_toobj(v));
|
snprintf(sbuf, sizeof(sbuf), "<function: %p>", var_toobj(v));
|
||||||
break;
|
break;
|
||||||
case BE_CLASS:
|
case BE_CLASS:
|
||||||
sprintf(sbuf, "<class: %s>",
|
snprintf(sbuf, sizeof(sbuf), "<class: %s>",
|
||||||
str(be_class_name(cast(bclass*, var_toobj(v)))));
|
str(be_class_name(cast(bclass*, var_toobj(v)))));
|
||||||
break;
|
break;
|
||||||
case BE_MODULE:
|
case BE_MODULE:
|
||||||
module2str(sbuf, v);
|
module2str(sbuf, sizeof(sbuf), v);
|
||||||
break;
|
break;
|
||||||
case BE_COMPTR:
|
case BE_COMPTR:
|
||||||
sprintf(sbuf, "<ptr: %p>", var_toobj(v));
|
snprintf(sbuf, sizeof(sbuf), "<ptr: %p>", var_toobj(v));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
strcpy(sbuf, "(unknown value)");
|
strncpy(sbuf, "(unknown value)", sizeof(sbuf));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return be_newstr(vm, sbuf);
|
return be_newstr(vm, sbuf);
|
||||||
@ -117,8 +117,10 @@ static bstring* ins2str(bvm *vm, int idx)
|
|||||||
be_incrtop(vm); /* push the obj::tostring to stack */
|
be_incrtop(vm); /* push the obj::tostring to stack */
|
||||||
if (basetype(type) != BE_FUNCTION) {
|
if (basetype(type) != BE_FUNCTION) {
|
||||||
bstring *name = be_class_name(be_instance_class(obj));
|
bstring *name = be_class_name(be_instance_class(obj));
|
||||||
char *sbuf = be_malloc(vm, (size_t)str_len(name) + 16);
|
size_t buf_len = (size_t) str_len(name) + 16;
|
||||||
sprintf(sbuf, "<instance: %s()>", str(name));
|
char *sbuf = be_malloc(vm, buf_len);
|
||||||
|
/* TODO what if sbuf cannot be allocated */
|
||||||
|
snprintf(sbuf, buf_len, "<instance: %s()>", str(name));
|
||||||
be_stackpop(vm, 1); /* pop the obj::tostring */
|
be_stackpop(vm, 1); /* pop the obj::tostring */
|
||||||
s = be_newstr(vm, sbuf);
|
s = be_newstr(vm, sbuf);
|
||||||
be_free(vm, sbuf, (size_t)str_len(name) + 16);
|
be_free(vm, sbuf, (size_t)str_len(name) + 16);
|
||||||
@ -217,7 +219,7 @@ const char* be_pushvfstr(bvm *vm, const char *format, va_list arg)
|
|||||||
}
|
}
|
||||||
case 'p': {
|
case 'p': {
|
||||||
char buf[2 * sizeof(void*) + 4];
|
char buf[2 * sizeof(void*) + 4];
|
||||||
sprintf(buf, "%p", va_arg(arg, void*));
|
snprintf(buf, sizeof(buf), "%p", va_arg(arg, void*));
|
||||||
pushstr(vm, buf, strlen(buf));
|
pushstr(vm, buf, strlen(buf));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -632,7 +634,7 @@ int be_str_format(bvm *vm)
|
|||||||
bint val;
|
bint val;
|
||||||
if (convert_to_int(vm, index, &val)) {
|
if (convert_to_int(vm, index, &val)) {
|
||||||
mode_fixlen(mode, BE_INT_FMTLEN);
|
mode_fixlen(mode, BE_INT_FMTLEN);
|
||||||
sprintf(buf, mode, val);
|
snprintf(buf, sizeof(buf), mode, val);
|
||||||
}
|
}
|
||||||
be_pushstring(vm, buf);
|
be_pushstring(vm, buf);
|
||||||
break;
|
break;
|
||||||
@ -642,7 +644,7 @@ int be_str_format(bvm *vm)
|
|||||||
{
|
{
|
||||||
breal val;
|
breal val;
|
||||||
if (convert_to_real(vm, index, &val)) {
|
if (convert_to_real(vm, index, &val)) {
|
||||||
sprintf(buf, mode, val);
|
snprintf(buf, sizeof(buf), mode, val);
|
||||||
}
|
}
|
||||||
be_pushstring(vm, buf);
|
be_pushstring(vm, buf);
|
||||||
break;
|
break;
|
||||||
@ -651,7 +653,7 @@ int be_str_format(bvm *vm)
|
|||||||
{
|
{
|
||||||
bint val;
|
bint val;
|
||||||
if (convert_to_int(vm, index, &val)) {
|
if (convert_to_int(vm, index, &val)) {
|
||||||
sprintf(buf, "%c", (int)val);
|
snprintf(buf, sizeof(buf), "%c", (int)val);
|
||||||
}
|
}
|
||||||
be_pushstring(vm, buf);
|
be_pushstring(vm, buf);
|
||||||
break;
|
break;
|
||||||
@ -662,7 +664,7 @@ int be_str_format(bvm *vm)
|
|||||||
if (len > 100 && strlen(mode) == 2) {
|
if (len > 100 && strlen(mode) == 2) {
|
||||||
be_pushvalue(vm, index);
|
be_pushvalue(vm, index);
|
||||||
} else {
|
} else {
|
||||||
sprintf(buf, mode, s);
|
snprintf(buf, sizeof(buf), mode, s);
|
||||||
be_pushstring(vm, buf);
|
be_pushstring(vm, buf);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -810,10 +812,10 @@ static int str_i2hex(bvm *vm)
|
|||||||
if (top >= 2 && be_isint(vm, 2)) {
|
if (top >= 2 && be_isint(vm, 2)) {
|
||||||
bint num = be_toint(vm, 2);
|
bint num = be_toint(vm, 2);
|
||||||
if (num > 0 && num <= 16) {
|
if (num > 0 && num <= 16) {
|
||||||
sprintf(fmt, "%%.%d" BE_INT_FMTLEN "X", (int)num);
|
snprintf(fmt, sizeof(fmt), "%%.%d" BE_INT_FMTLEN "X", (int)num);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sprintf(buf, fmt, value);
|
snprintf(buf, sizeof(buf), fmt, value);
|
||||||
be_pushstring(vm, buf);
|
be_pushstring(vm, buf);
|
||||||
be_return(vm);
|
be_return(vm);
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,6 @@ class Matter_UI
|
|||||||
#- ---------------------------------------------------------------------- -#
|
#- ---------------------------------------------------------------------- -#
|
||||||
def show_enable()
|
def show_enable()
|
||||||
import webserver
|
import webserver
|
||||||
import string
|
|
||||||
var matter_enabled = self.matter_enabled
|
var matter_enabled = self.matter_enabled
|
||||||
|
|
||||||
webserver.content_send("<fieldset><legend><b> Matter </b></legend>"
|
webserver.content_send("<fieldset><legend><b> Matter </b></legend>"
|
||||||
@ -97,12 +96,14 @@ class Matter_UI
|
|||||||
"<form action='/matterc' method='post'>")
|
"<form action='/matterc' method='post'>")
|
||||||
|
|
||||||
# checkbox for Matter enable
|
# checkbox for Matter enable
|
||||||
webserver.content_send(string.format("<p><input id='menable' type='checkbox' name='menable' %s>", self.matter_enabled() ? "checked" : ""))
|
var matter_enabled_checked = self.matter_enabled() ? 'checked' : ''
|
||||||
|
webserver.content_send(f"<p><input id='menable' type='checkbox' name='menable' {matter_enabled_checked}>")
|
||||||
webserver.content_send("<label for='menable'><b>Matter enable</b></label></p>")
|
webserver.content_send("<label for='menable'><b>Matter enable</b></label></p>")
|
||||||
|
|
||||||
if self.matter_enabled()
|
if self.matter_enabled()
|
||||||
# checkbox for Matter commissioning
|
# checkbox for Matter commissioning
|
||||||
webserver.content_send(string.format("<p><input id='comm' type='checkbox' name='comm' %s>", self.device.commissioning_open != nil ? "checked" : ""))
|
var commissioning_open_checked = self.device.commissioning_open != nil ? "checked" : ""
|
||||||
|
webserver.content_send(f"<p><input id='comm' type='checkbox' name='comm' {commissioning_open_checked}>")
|
||||||
webserver.content_send("<label for='comm'><b>Commissioning open</b></label></p>")
|
webserver.content_send("<label for='comm'><b>Commissioning open</b></label></p>")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -163,21 +164,20 @@ class Matter_UI
|
|||||||
#- ---------------------------------------------------------------------- -#
|
#- ---------------------------------------------------------------------- -#
|
||||||
def show_commissioning_info()
|
def show_commissioning_info()
|
||||||
import webserver
|
import webserver
|
||||||
import string
|
|
||||||
|
|
||||||
var seconds_left = (self.device.commissioning_open - tasmota.millis()) / 1000
|
var seconds_left = (self.device.commissioning_open - tasmota.millis()) / 1000
|
||||||
if seconds_left < 0 seconds_left = 0 end
|
if seconds_left < 0 seconds_left = 0 end
|
||||||
var min_left = (seconds_left + 30) / 60
|
var min_left = (seconds_left + 30) / 60
|
||||||
|
|
||||||
webserver.content_send(string.format("<fieldset><legend><b> Commissioning open for %i min </b></legend><p></p>", min_left))
|
webserver.content_send(f"<fieldset><legend><b> Commissioning open for {min_left:i} min </b></legend><p></p>")
|
||||||
|
|
||||||
var pairing_code = self.device.compute_manual_pairing_code()
|
var pairing_code = self.device.compute_manual_pairing_code()
|
||||||
webserver.content_send(string.format("<p>Manual pairing code:<br><b>%s-%s-%s</b></p><hr>", pairing_code[0..3], pairing_code[4..6], pairing_code[7..]))
|
webserver.content_send(f"<p>Manual pairing code:<br><b>{pairing_code[0..3]}-{pairing_code[4..6]}-{pairing_code[7..]}</b></p><hr>")
|
||||||
|
|
||||||
webserver.content_send("<div><center>")
|
webserver.content_send("<div><center>")
|
||||||
var qr_text = self.device.compute_qrcode_content()
|
var qr_text = self.device.compute_qrcode_content()
|
||||||
self.show_qrcode(qr_text)
|
self.show_qrcode(qr_text)
|
||||||
webserver.content_send(string.format("<p> %s</p>", qr_text))
|
webserver.content_send(f"<p> {qr_text}</p>")
|
||||||
webserver.content_send("</div><p></p></fieldset><p></p>")
|
webserver.content_send("</div><p></p></fieldset><p></p>")
|
||||||
|
|
||||||
end
|
end
|
||||||
@ -187,16 +187,16 @@ class Matter_UI
|
|||||||
#- ---------------------------------------------------------------------- -#
|
#- ---------------------------------------------------------------------- -#
|
||||||
def show_passcode_form()
|
def show_passcode_form()
|
||||||
import webserver
|
import webserver
|
||||||
import string
|
|
||||||
|
|
||||||
webserver.content_send("<fieldset><legend><b> Matter Advanced Configuration </b></legend><p></p>")
|
webserver.content_send("<fieldset><legend><b> Matter Advanced Configuration </b></legend><p></p>")
|
||||||
#
|
#
|
||||||
webserver.content_send("<form action='/matterc' method='post' onsubmit='return confirm(\"This will cause a restart.\");'>"
|
webserver.content_send("<form action='/matterc' method='post' onsubmit='return confirm(\"This will cause a restart.\");'>"
|
||||||
"<p>Passcode:</p>")
|
"<p>Passcode:</p>")
|
||||||
webserver.content_send(string.format("<input type='number' min='1' max='99999998' name='passcode' value='%i'>", self.device.root_passcode))
|
webserver.content_send(f"<input type='number' min='1' max='99999998' name='passcode' value='{self.device.root_passcode:i}'>")
|
||||||
webserver.content_send("<p>Distinguish id:</p>")
|
webserver.content_send("<p>Distinguish id:</p>")
|
||||||
webserver.content_send(string.format("<input type='number' min='0' max='4095' name='discriminator' value='%i'>", self.device.root_discriminator))
|
webserver.content_send(f"<input type='number' min='0' max='4095' name='discriminator' value='{self.device.root_discriminator:i}'>")
|
||||||
webserver.content_send(string.format("<p><input type='checkbox' name='ipv4'%s>IPv4 only</p>", self.device.ipv4only ? " checked" : ""))
|
var ipv4only_checked = self.device.ipv4only ? " checked" : ""
|
||||||
|
webserver.content_send(f"<p><input type='checkbox' name='ipv4'{ipv4only_checked}>IPv4 only</p>")
|
||||||
webserver.content_send("<p></p><button name='passcode' class='button bgrn'>Change</button></form></p>"
|
webserver.content_send("<p></p><button name='passcode' class='button bgrn'>Change</button></form></p>"
|
||||||
"<p></p></fieldset><p></p>")
|
"<p></p></fieldset><p></p>")
|
||||||
|
|
||||||
@ -224,15 +224,15 @@ class Matter_UI
|
|||||||
if !label label = "<No label>" end
|
if !label label = "<No label>" end
|
||||||
label = webserver.html_escape(label) # protect against HTML injection
|
label = webserver.html_escape(label) # protect against HTML injection
|
||||||
|
|
||||||
webserver.content_send(string.format("<fieldset><legend><b> #%i %s</b> (%s) </legend><p></p>", f.get_fabric_index(), label, f.get_admin_vendor_name()))
|
webserver.content_send(f"<fieldset><legend><b> #{f.get_fabric_index():i} {label}</b> ({f.get_admin_vendor_name()}) </legend><p></p>")
|
||||||
|
|
||||||
var fabric_rev = f.get_fabric_id().copy().reverse()
|
var fabric_rev = f.get_fabric_id().copy().reverse()
|
||||||
var deviceid_rev = f.get_device_id().copy().reverse()
|
var deviceid_rev = f.get_device_id().copy().reverse()
|
||||||
webserver.content_send(string.format("Fabric: %s<br>", fabric_rev.tohex()))
|
webserver.content_send(f"Fabric: {fabric_rev.tohex()}<br>")
|
||||||
webserver.content_send(string.format("Device: %s<br> ", deviceid_rev.tohex()))
|
webserver.content_send(f"Device: {deviceid_rev.tohex()}<br> ")
|
||||||
|
|
||||||
webserver.content_send("<form action='/matterc' method='post' onsubmit='return confirm(\"Are you sure?\");'>")
|
webserver.content_send("<form action='/matterc' method='post' onsubmit='return confirm(\"Are you sure?\");'>")
|
||||||
webserver.content_send(string.format("<input name='del_fabric' type='hidden' value='%i'>", f.get_fabric_index()))
|
webserver.content_send(f"<input name='del_fabric' type='hidden' value='{f.get_fabric_index():i}'>")
|
||||||
webserver.content_send("<button name='del' class='button bgrn'>Delete Fabric</button></form></p>")
|
webserver.content_send("<button name='del' class='button bgrn'>Delete Fabric</button></form></p>")
|
||||||
|
|
||||||
webserver.content_send("<p></p></fieldset><p></p>")
|
webserver.content_send("<p></p></fieldset><p></p>")
|
||||||
@ -252,8 +252,8 @@ class Matter_UI
|
|||||||
# var hl = ["Enter Relay number","Not used","Enter Filter pattern","Enter Switch number"];
|
# var hl = ["Enter Relay number","Not used","Enter Filter pattern","Enter Switch number"];
|
||||||
def show_plugins_hints_js(*class_list)
|
def show_plugins_hints_js(*class_list)
|
||||||
import webserver
|
import webserver
|
||||||
import string
|
|
||||||
import json
|
import json
|
||||||
|
import string
|
||||||
|
|
||||||
var class_types = []
|
var class_types = []
|
||||||
for cl: class_list
|
for cl: class_list
|
||||||
@ -279,11 +279,11 @@ class Matter_UI
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
webserver.content_send(string.format(
|
webserver.content_send(f""
|
||||||
"<script type='text/javascript'>"
|
"<script type='text/javascript'>"
|
||||||
"var hm=%s;"
|
"var hm={json.dump(hm)};"
|
||||||
"var hl=%s;"
|
"var hl={json.dump(hl)};"
|
||||||
"</script>", json.dump(hm), json.dump(hl)))
|
"</script>")
|
||||||
|
|
||||||
webserver.content_send(matter._ADD_ENDPOINT_JS)
|
webserver.content_send(matter._ADD_ENDPOINT_JS)
|
||||||
|
|
||||||
@ -332,17 +332,17 @@ class Matter_UI
|
|||||||
end
|
end
|
||||||
|
|
||||||
found = true
|
found = true
|
||||||
webserver.content_send(string.format("<tr><td style='font-size:smaller;'><b>%i</b></td>", ep))
|
webserver.content_send(f"<tr><td style='font-size:smaller;'><b>{ep:i}</b></td>")
|
||||||
webserver.content_send(string.format("<td style='font-size:smaller;'><input type='text' name='nam%i' size='1' value='%s'></td>",
|
webserver.content_send(format("<td style='font-size:smaller;'><input type='text' name='nam%i' size='1' value='%s'></td>",
|
||||||
ep, webserver.html_escape(conf.find('name', ''))))
|
ep, webserver.html_escape(conf.find('name', ''))))
|
||||||
webserver.content_send(string.format("<td style='font-size:smaller;'><b>%s</b></td>", self.plugin_name(conf.find('type', ''))))
|
webserver.content_send(f"<td style='font-size:smaller;'><b>{self.plugin_name(conf.find('type', ''))}</b></td>")
|
||||||
webserver.content_send(string.format("<td style='font-size:smaller;'><input type='text' name='arg%i' size='1' value='%s' placeholder='%s'></td>",
|
webserver.content_send(format("<td style='font-size:smaller;'><input type='text' name='arg%i' size='1' value='%s' placeholder='%s'></td>",
|
||||||
ep, webserver.html_escape(arg), cl ? webserver.html_escape(cl.ARG_HINT) : ''))
|
ep, webserver.html_escape(arg), cl ? webserver.html_escape(cl.ARG_HINT) : ''))
|
||||||
webserver.content_send(string.format("<td style='text-align:center;'><button name='del%i' "
|
webserver.content_send(f"<td style='text-align:center;'><button name='del{ep:i}' "
|
||||||
"style='background:none;border:none;line-height:1;'"
|
"style='background:none;border:none;line-height:1;'"
|
||||||
" onclick=\"return confirm('Confirm removing endpoint')\""
|
" onclick=\"return confirm('Confirm removing endpoint')\""
|
||||||
">"
|
">"
|
||||||
"🔥</button></td></tr>", ep))
|
"🔥</button></td></tr>")
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
webserver.content_send("</table>")
|
webserver.content_send("</table>")
|
||||||
@ -366,7 +366,8 @@ class Matter_UI
|
|||||||
|
|
||||||
for remote: remotes
|
for remote: remotes
|
||||||
|
|
||||||
webserver.content_send(string.format("🔗 <a target='_blank' href=\"http://%s/?\">%s</a>", webserver.html_escape(remote), webserver.html_escape(remote)))
|
var remote_html = webserver.html_escape(remote)
|
||||||
|
webserver.content_send(f"🔗 <a target='_blank' href=\"http://{remote_html}/?\">{remote_html}</a>")
|
||||||
webserver.content_send("<table style='width:100%'>")
|
webserver.content_send("<table style='width:100%'>")
|
||||||
webserver.content_send("<tr>"
|
webserver.content_send("<tr>"
|
||||||
"<td width='25'></td>"
|
"<td width='25'></td>"
|
||||||
@ -396,18 +397,18 @@ class Matter_UI
|
|||||||
end
|
end
|
||||||
|
|
||||||
found = true
|
found = true
|
||||||
webserver.content_send(string.format("<tr><td width='22' style='font-size:smaller;'><b>%i</b></td>", ep))
|
webserver.content_send(f"<tr><td width='22' style='font-size:smaller;'><b>{ep:i}</b></td>")
|
||||||
webserver.content_send(string.format("<td width='78' style='font-size:smaller;'><input type='text' name='nam%i' size='1' value='%s' placeholder='(optional)'></td>",
|
webserver.content_send(format("<td width='78' style='font-size:smaller;'><input type='text' name='nam%i' size='1' value='%s' placeholder='(optional)'></td>",
|
||||||
ep, webserver.html_escape(conf.find('name', ''))))
|
ep, webserver.html_escape(conf.find('name', ''))))
|
||||||
|
|
||||||
webserver.content_send(string.format("<td width='115' style='font-size:smaller;'><b>%s</b></select></td>", self.plugin_name(conf.find('type', ''))))
|
webserver.content_send(format("<td width='115' style='font-size:smaller;'><b>%s</b></select></td>", self.plugin_name(conf.find('type', ''))))
|
||||||
webserver.content_send(string.format("<td style='font-size:smaller;'><input type='text' name='arg%i' size='8' value='%s'></td>",
|
webserver.content_send(format("<td style='font-size:smaller;'><input type='text' name='arg%i' size='8' value='%s'></td>",
|
||||||
ep, webserver.html_escape(arg)))
|
ep, webserver.html_escape(arg)))
|
||||||
webserver.content_send(string.format("<td width='15' style='text-align:center;'><button name='del%i' "
|
webserver.content_send(f"<td width='15' style='text-align:center;'><button name='del{ep:i}' "
|
||||||
"style='background:none;border:none;line-height:1;'"
|
"style='background:none;border:none;line-height:1;'"
|
||||||
" onclick=\"return confirm('Confirm removing endpoint')\""
|
" onclick=\"return confirm('Confirm removing endpoint')\""
|
||||||
">"
|
">"
|
||||||
"🔥</button></td></tr>", ep))
|
"🔥</button></td></tr>")
|
||||||
i += 1
|
i += 1
|
||||||
end
|
end
|
||||||
webserver.content_send("</table><p></p>")
|
webserver.content_send("</table><p></p>")
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user