From 3cff0f1ab97bf61a53cc9c74b5e259947b3e2861 Mon Sep 17 00:00:00 2001 From: M.Gergő Date: Wed, 6 May 2020 23:03:17 +0200 Subject: LightConfINI rewrite & rework --- src/deftypes.h | 28 +++ src/ini_read.c | 676 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ini_rw.h | 54 +++++ src/main.c | 56 +++++ 4 files changed, 814 insertions(+) create mode 100644 src/deftypes.h create mode 100644 src/ini_read.c create mode 100644 src/ini_rw.h create mode 100644 src/main.c (limited to 'src') diff --git a/src/deftypes.h b/src/deftypes.h new file mode 100644 index 0000000..dbfd1e0 --- /dev/null +++ b/src/deftypes.h @@ -0,0 +1,28 @@ + +#ifndef _DEFTYPESH_INCLUDED +#define _DEFTYPESH_INCLUDED + +// Unsigned types +#define uint8 __uint8_t +#define uint16 __uint16_t +#define uint32 __uint32_t +#define uint64 __uint64_t +#define uint128 __uint128_t + +// Signed types (default) +#define int8 __int8_t +#define int16 __int16_t +#define int32 __int32_t +#define int64 __int64_t +#define int128 __int128_t + + +#define uchar __u_char + +#define ulong __u_long + + +#define TRUE 1 +#define FALSE 0 + +#endif \ No newline at end of file diff --git a/src/ini_read.c b/src/ini_read.c new file mode 100644 index 0000000..a33189b --- /dev/null +++ b/src/ini_read.c @@ -0,0 +1,676 @@ +/*INI fájl olvasás */ + +#include +#include /* strncpy + strerror*/ +#include /* malloc(), atoi(), exit(EXIT_FALIURE)*/ +#include /*, isspace(), tolower() */ +#include /*errno*/ +#include +#include /* int64_t*/ + + + +#define ini_read_c +#include "ini_rw.h" + +enum ini_states state = Start; +struct lci_data fsmdata; + + +size_t strNullLen(const char *str){ + if(str == NULL){ + return 0; + } else { + return strlen(str); + } +} + +char *strResize(char *ptr, size_t oldsize, size_t newsize){ + char *tmp; + + if(newsize == 0){ //pucol + free(ptr); + return NULL; + + } else if(newsize != oldsize){ //Változtat + tmp = (char *) malloc(newsize*sizeof(char)); + if(tmp == NULL){ //Hiba esetén nem nyúl hozzá + return ptr; + } else if(ptr == NULL) { + memset(tmp, 0, newsize); + ptr = tmp; + return tmp; + } else if(newsize > oldsize) { // FEL + memset(tmp, 0, newsize); + strncpy(tmp, ptr, oldsize); // old < new + free(ptr); + ptr = tmp; + return tmp; + } else if(newsize < oldsize){ //LE + memset(tmp, 0, newsize); + strncpy(tmp, ptr, newsize); // new < old + free(ptr); + ptr = tmp; + return tmp; + } else { //Ide sosem jutunk + return ptr; + } + } else { + return ptr; + } +} + +int checkspace(int in){ + switch (in){ + case ' ': + return 0x20; + case '\t': + return 0x09; + case '\v': + return 0x0b; + case '\f': + return 0x0c; + case '\r': + return 0x0d; + default: + return 0; + } + +} + +char unescape(char c){ + if(c == 'n'){ //Newline + return '\n'; + } else if(c == 'a'){ //Bell + return '\a'; + } else if(c == 'b'){ //Backspace + return '\b'; + } else if(c == 'f'){ //Formfeed Page Break + return '\f'; + } else if(c == 'r'){ //Carrige return + return '\r'; + } else if(c == 't'){ //Horizontal tab + return '\t'; + } else if(c == 'v'){ //Vertical tab + return '\v'; + } else if(c == '\\'){ //Backslash + return '\\'; + } else if(c == '\''){ //Apostrophe + return '\''; + } else if(c == '\"'){ //Double quotation mark + return '\"'; + } else if(c == '?'){ //Question mark + return '?'; + } else if(c < 0x20){ + return '~'; + } else { // jó az eredeti + return c; + } +} + + +int64_t getFileMaxLineLen(FILE *tfd){ + + int64_t c=0; + int64_t i=0, max=0; + + if (tfd != NULL){ + while( c != EOF){ + c = fgetc(tfd); + if (c == '\n' || c == EOF) { //Line End + i++; // An "\n" is a character + if(max < i){ + max = i; + } + i = 0; // Recount on every newline + } else { + i++; + } + } + fseek(tfd, 0, SEEK_SET); //vissza az elejére + return max; + } else { // File open error + return -1; + } +} + + +struct lci_data *iniFSM(struct lci_data *data, const char *in, int64_t len){ + + int64_t i,j; + enum ini_states pstate=Start, state=Start; + char cc; + + if(data == NULL){ + return NULL; + } else { + + for(i=0, j=0; inodeState == MULTILINE){ //Elküldjükgyűjteni + state = DqmW; // Vagy ValW ?? + j = data->valueLen-2; //(x-1) => \0; (x-2) => j--; + data->value = strResize(data->value, data->valueLen, data->valueLen+len); + i--; + //j--; + } else if(in[i] == '\n' || in[i] == '\r' || in[i] == '\0'){ //sorvége + state = Stop; // marad helyben + i--; + } else if(isspace(in[i])){ //ISSPACE, de nem sorvége + state = BgnSp; + } else if(in[i] == ';' || in[i] == '#' ){ //Komment jel az első + j = -1; + state = CommEndW; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->sectionLen = 0; //Hogy pucoljon + } else if(in[i] == '['){ //Szekció kezdődik + j = -1; + state = SectEndW; + data->section = strResize(data->section, data->sectionLen, len); + memset(data->section, 0, len); + data->sectionStartPos = i; //zárójeleket is veszi! + } else if(isalnum(in[i]) ){ //Változó jön + j = -1; + state = EqW1; + data->param = strResize(data->param, data->paramLen, len); + memset(data->param, 0, len); + data->paramStartPos = i; + i--; + } else { + state = Error; + i--; + // control karakterek az 1. pos-ban + } + data->nodeState = EMPTY; + pstate = Start; + break; + + + case BgnSp: + if (in[i] == '\n' || in[i] == '\r' ){ + state = Stop; + i--; + } else if (in[i] == ';' || in[i] == '#'){ //Komment jön + j = -1; + state = CommEndW; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->sectionLen = 0; //Hogy pucoljon + } else if(in[i] == '['){ //Szekció lesz + j = -1; + state = SectEndW; + data->section = strResize(data->section, data->sectionLen, len); + memset(data->section, 0, len); + data->sectionStartPos = i; + } else if (isalnum(in[i])){ //Változó lesz belőle + j = -1; + state = EqW1; + data->param = strResize(data->param, data->paramLen, len); + memset(data->param, 0, len); + data->paramStartPos = i; + i--; + } else if(isspace(in[i]) /*|| in[i] == '\0' */ ){ + state = BgnSp; + } else { + state = Error; + i--; + } + pstate = BgnSp; + break; + + + case CommEndW: + if(in[i] == '\n' || in[i] == '\r'){ + state = Stop; + data->nodeState = READY; + data->comment[j] = '\0'; + data->commentLen = j+1; + i--; + } else { + data->comment[j] = in[i]; + } + pstate = CommEndW; + break; + + + case SectEndW: // Sectiont gyűjti + if(in[i] == ']'){ + state = SectEndD; + data->section[j] = '\0'; + data->sectionLen = j+1; + } else if(isalnum(in[i])){ + data->section[j] = in[i]; + } else if((in[i] >= 0x00 && in[i] < 0x20) || in[i] == ';' || in[i] == '#' ) { // túl korai sorvég, SP(), vagy komment + state = Error; + data->section[j] = '\0'; + data->sectionLen = j+1; + i--; + } else { + data->section[j] = in[i]; + } + pstate = SectEndW; + break; + + + case SectEndD: //Section begyűjtve, utána + if(in[i] == '\n' || in[i] == '\r'){ + state = Stop; + data->nodeState = READY; + i--; + }else if(isspace(in[i])){ + state = SectEndD; //marad helyben + } else if (in[i] == ';' || in[i] == '#'){ + j = -1; + state = CommEndW; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->nodeState = CONTINUE; + } else { + state = Error; //hibás sor + i--; + //memset(data->section, 0, len); // legyen-e érvényes a section? + } + pstate = SectEndD; + break; + + + case EqW1: //Egyenlő előtti (változó) + if(in[i] == '='){ //végén egyenlő + state = ValPSP; + data->param[j] = '\0'; + data->paramLen = j + 1; + } else if(isalnum(in[i]) || in[i] == '[' || in[i] == ']'){ //A paraméter neve (PHP támogatással) + data->param[j] = in[i]; + } else if(isspace(in[i]) && in[i] != '\r' && in[i] != '\n'){ //Végén csak space lehet + state = EqW2; + data->param[j] = '\0'; + data->paramLen = j + 1; + } else { + state = Error; + i--; + } + pstate = EqW1; + break; + + + case EqW2: //Egyenlő előtti (space) + if(in[i] == '='){ //Végre egyenlő + j = -1; + state = ValPSP; + } else if(isspace(in[i])){ //marad helyben + state = EqW2; + } else { + state = Error; + i--; + } + pstate = EqW2; + break; + + + case ValPSP: //EqD //megvolt az egyenlő, érték előtti space + if(in[i] == '\n' || in[i] == '\r' /*|| in[i] == '\0'*/ ){ //ekkor üres változó + state = Stop; + data->nodeState = READY; + i--; + } else if(in[i] == ';' || in[i] == '#'){ //komment jön + j = -1; + state = CommEndW; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->nodeState = CONTINUE; + + } else if( pstate != Bslsh && in[i] == '\"' ){ //Macskaköröm jött + j = -1; + state = DqmW; + data->value = strResize(data->value, data->valueLen, len); + memset(data->value, 0, len); + data->valueStartPos = i; + data->nodeState = CONTINUE; + } else if(isspace(in[i])){ //SPACE jött -> marad helyben + state = ValPSP; + } else if(isalnum(in[i])) { //Változó macskaköröm nélkül + j = -1; + state = ValW; + data->value = strResize(data->value, data->valueLen, len); + memset(data->value, 0, len); + data->valueStartPos = i; + data->nodeState = CONTINUE; + i--; + } else { + state = Error; + i--; + } + pstate = ValPSP; + break; + + + case ValW: //Érték kigyűjtése + if(in[i] == '\n' || in[i] == '\r'){ //vége -> új sor + state = Stop; + data->nodeState = READY; + data->value[j] = '\0'; + data->valueLen = j + 1; + i--; + } else if(in[i] == ';' || in[i] == '#'){ //comment + data->nodeState = CONTINUE; + data->value[j] = '\0'; + data->valueLen = j+1; + state = CommEndW; + j = -1; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->nodeState = CONTINUE; + /* } else if( pstate != Bslsh && in[i] == '\"' ){ //Macskaköröm jött + j = -1; + state = DqmW; + data->nodeState = CONTINUE; + */ + } else if( /* pstate != DqmW &&*/ /*pstate != Bslsh &&*/ in[i] == '\\' ){ //Backslash jött //egymás után több backslash is lehet?? + j--; //A '\' nem számít bele! + state = Bslsh; + //} else if( pstate == Bslsh ){ //Backslash-ból jöttünk vissza + //data->value[j] = eescape(c); + } else if( /*pstate != DqmW &&*/ isalnum(in[i])){ //Normál betűk, space() csak idézőjelben + data->value[j] = in[i]; + } else if(isspace(in[i])){ //Aposztróf nélüli space -> sorvég + //if(pstate != DqmW){ + data->value[j] = '\0'; + data->valueLen = j+1; + data->nodeState = READY; + //} + state = ValFSP; + i--; + } else { + state = Error; + i--; + } + pstate = ValW; + break; + + + case Bslsh: //Backslash -> escape-re lesz szükség + if(pstate == ValW){ + state = ValW; + data->value[j] = unescape(in[i]); + } else if(pstate == SectEndW){ + state = SectEndW; + data->section[j] = unescape(in[i]); + }else if(pstate == DqmW){ + state = DqmW; + data->value[j] = unescape(in[i]); + if(in[i] == '\n'){ //Workaround for UNIX line end + i--; + } + } else { + state = Error; + i--; + } + if(in[i] < 0x20){ //láthatatlan: pl \\n (escapelt sorvége) + j--; + } + pstate = Bslsh; + break; + + + case ValFSP: //Érték utáni space + if( in[i] == '\n' || in[i] == '\r'){ + state = Stop; + i--; + } else if( in[i] == ';' || in[i] == '#'){ + j = -1; + state = CommEndW; + data->comment = strResize(data->comment, data->commentLen, len); + memset(data->comment, 0, len); + data->commentStartPos = i; + data->nodeState = CONTINUE; + } else if(isspace(in[i])){ + state = ValFSP; //marad helyben + } else { + state = Error; + i--; + } + pstate = ValFSP; + break; + + + case DqmW: //dupla idézőjelben az érték + if(pstate != Bslsh && in[i] == '\"'){ //újabb idézőjel -> string vége + data->value[j] = '\0'; + data->valueLen = j+1; + data->nodeState = READY; + // state = ValW; //Ha több, egymás melletti idézőjeles részt is akarunk + state = ValFSP; + } else if (/*pstate != Bslsh &&*/ in[i] == '\\'){ //Backslash jött + j--; + state = Bslsh; + } else if( in[i] == '\n' && in[i-1] == '\\'){ ///UNIX style + //j--; + data->value[j-1] = '\0'; // '\' => '\0' + data->valueLen = j+1-1; + data->nodeState = MULTILINE; + state = Stop; + i--; + } else if( in[i] == '\n' && in[i-1] == '\r' && in[i-2] == '\\'){ //WINDOWS style + data->value[j] = '\0'; + data->valueLen = j+1; + data->nodeState = MULTILINE; + state = Stop; + i--; + } else if( pstate != Bslsh && ( in[i] == '\r' || in[i] == '\n' || in[i] == '\0')){ //túl korai sorvége + state = Error; + i--; + } else { + data->value[j] = in[i]; + } + pstate = DqmW; + break; + + + case Error: + data->errorMsg = strResize(data->errorMsg, data->errorMsgLen, 256); + memset(data->errorMsg, 0, 256) ; + + if(pstate == SectEndW || pstate == SectEndD){ + data->errorMsgLen = snprintf(data->errorMsg, 256, "Illegal character at SECTION! (line: %ld, pos: %ld)", data->lineNum, i+1); + } else if(pstate == EqW1 || pstate == EqW2){ + data->errorMsgLen = snprintf(data->errorMsg, 256, "Illegal character at PARAMETER! (line: %ld, pos: %ld)", data->lineNum, i+1); + } else if(pstate == ValPSP || pstate == ValW || pstate == ValFSP ){ + data->errorMsgLen = snprintf(data->errorMsg, 256, "Illegal character at VALUE! (line: %ld, pos: %ld)", data->lineNum, i+1); + } else if(pstate == DqmW){ + data->errorMsgLen = snprintf(data->errorMsg, 256, "Double quotation mark needed! (line: %ld, pos: %ld)", data->lineNum, i+1); + } else { //pstate == Stop + data->errorMsgLen = snprintf(data->errorMsg, 256, "Illegal character! (line: %ld, pos: %ld)", data->lineNum, i+1); + } + + i--; + state = Stop; + data->nodeState = ERROR; + pstate = Error; + break; + + case Stop: + if(in[i] == '\n' || in[i] == '\r' || in[i] == '\0' || pstate == Error){ //Sorvége, maradunk + state = Stop; + //} else if(isgraph(c)) { //Látható karakter -> újabb adag + // i--; + // state = Start; + //} else if(in[i] == '\0'){ + if(strNullLen(data->section) == 0){data->sectionLen = 0;} + if(strNullLen(data->param) == 0){data->paramLen = 0;} + if(strNullLen(data->value) == 0){data->valueLen = 0;} + if(strNullLen(data->comment) == 0){data->commentLen = 0;} + if(strNullLen(data->errorMsg) == 0){data->errorMsgLen = 0;} + data->section = strResize(data->section, len, data->sectionLen); + data->param = strResize(data->param, len, data->paramLen); + data->value = strResize(data->value, len, data->valueLen); + data->comment = strResize(data->comment, len, data->commentLen); + data->errorMsg = strResize(data->errorMsg, 256, data->errorMsgLen); + return data; + } else { //minden más-> hiba + state = Error; + } + pstate = Stop; + break; + + default: + state = Start; + break; + } + } + return data; + } +} + + + + +lci_data *createNode( lci_data *head, int64_t lineLen ){ + lci_data *curr; + + curr = (lci_data *) malloc(1*sizeof(lci_data)); + curr->nodeState = EMPTY; + curr->lineNum = 0; + curr->lineLen = lineLen; + curr->section = (char *) malloc(lineLen*sizeof(char)); + curr->param = (char *) malloc(lineLen*sizeof(char)); + curr->value = (char *) malloc(lineLen*sizeof(char)); + curr->comment = (char *) malloc(lineLen*sizeof(char)); + curr->errorMsg = (char *) malloc(lineLen*sizeof(char)); + + memset(curr->section, 0, lineLen); + curr->sectionLen = lineLen; + curr->sectionStartPos = 0; + memset(curr->param, 0, lineLen); + curr->paramLen = lineLen; + curr->paramStartPos = 0; + memset(curr->value, 0, lineLen); + curr->valueLen = lineLen; + curr->valueStartPos = 0; + memset(curr->comment, 0, lineLen); + curr->commentLen = lineLen; + curr->commentStartPos = 0; + memset(curr->errorMsg, 0, lineLen); + curr->errorMsgLen=lineLen; + curr->next = NULL; + + if(head != NULL){ + head->next = curr; + } + return curr; +} + +lci_data *destroyNodes( lci_data *head){ + lci_data *tmp, *node=head; + while(node != NULL){ + tmp = node; + free(tmp->section); + free(tmp->param); + free(tmp->value); + free(tmp->comment); + free(tmp->errorMsg); + free(tmp); + node = node->next; + } + return NULL; +} + +struct lci_data *iniReadOut(const char *filename){ + + int c=0; + FILE *fp=NULL; + int64_t linemax, line=0, pos=0; + char *buff, cc; + struct lci_data *prev=NULL, *curr=NULL, *list = NULL; + + fp = fopen(filename, "rb"); + if(fp == NULL){ + list = createNode(NULL, 120); + list->errorMsgLen = snprintf(list->errorMsg, 120, "File opening error. Errno: %d (%s)", errno, strerror(errno) ); + list->nodeState = ERROR; + //return list; + } else { + linemax = getFileMaxLineLen(fp); + //list = createNode(NULL, linemax); + buff = (char *) malloc(linemax*sizeof(char)); + memset(buff, 0, linemax); + + + while( c != EOF){ + c = fgetc(fp); + cc = c; + + if( c == '\n' || c == EOF){ //Ha sorvége + line++; + if(c == EOF){ + buff[pos] = '\n'; + } else { + buff[pos] = cc; + } + + if(curr == NULL || curr->nodeState != MULTILINE ){ + curr = createNode(NULL, linemax); + } + if(list == NULL){ + list = curr; + } + if(prev != NULL && prev->section != NULL && curr != NULL && curr->section != NULL){ + strncpy(curr->section, prev->section, curr->lineLen); + curr->sectionLen = prev->sectionLen; + } + if(curr != NULL){ + curr->lineNum = line; + curr->lineLen = pos + 1; + curr = iniFSM(curr, buff, linemax); + } + if(curr->nodeState == EMPTY){ //üres sorokat eldobjuk + if(prev == curr || list == curr){ //Ha csak az az egy van a listában + list = NULL; + prev = NULL; + } + curr = destroyNodes(curr); + //} else if(curr->nodeState = ERROR){ + //destroyNodes(list); + //} else if (curr->nodeState == MULTILINE){ + //multiline + } else { + //???????? + } + + + if(prev == NULL){ //Ha még üres + prev = curr; + } else { //Az előzőhöz hozzávesszük a mostanit + prev->next = curr; + if(prev->next != NULL){ //Csak, ha lett új felvéve + prev = prev->next; + } + } + + pos = 0; + memset(buff, 0, linemax); + } else { + buff[pos] = c; + pos++; + } + } + free(buff); + } + + if(fp != NULL){ + fclose(fp); + } + return list; +} + + + + diff --git a/src/ini_rw.h b/src/ini_rw.h new file mode 100644 index 0000000..3e23e03 --- /dev/null +++ b/src/ini_rw.h @@ -0,0 +1,54 @@ +#ifndef INI_READ_H_INCLUDED +#define INI_READ_H_INCLUDED +// #include "deftypes.h" +#include /* int64_t*/ + + + +typedef struct lci_data { + + enum nodeState {EMPTY, READY, CONTINUE, MULTILINE, ERROR } nodeState; + uint64_t lineNum; + uint64_t lineLen; + + char *section; + uint64_t sectionLen; + uint64_t sectionStartPos; + char *param; + uint64_t paramLen; + uint64_t paramStartPos; + char *value; + uint64_t valueLen; + uint64_t valueStartPos; + char *comment; + uint64_t commentLen; + uint64_t commentStartPos; + char *errorMsg; + uint64_t errorMsgLen; + + struct lci_data *next; +} lci_data; + +enum ini_states {Start, BgnSp, CommEndW, SectEndW, SectEndD, EqW1, EqW2, ValPSP, ValW, ValFSP, DqmW, Bslsh, Error, Stop }; + +struct lci_data *iniReadOut(const char *filename); +int64_t getFileMaxLineLen(FILE *tfd); + + + +char *lciGETtoStr( const char *section, const char *param, char *dest, size_t dstlen ); +//int lciGETtoStrlen(const char *section, const char *param, ...); +int8_t lciGETtoInt8( const char *filename, const char *section, const char *param); +int16_t lciGETtoInt16(const char *filename, const char *section, const char *param); +int32_t lciGETtoInt32(const char *filename, const char *section, const char *param); +int64_t lciGETtoInt64(const char *filename, const char *section, const char *param); + +double lciGETtoDlb(const char *filename, const char *section, const char *param); +float lciGETtoFlt(const char *filename, const char *section, const char *param); +long int lciGETtoLng(const char *filename, const char *section, const char *param); + + + + +#endif // INI_READ_H_INCLUDED + diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1e6246f --- /dev/null +++ b/src/main.c @@ -0,0 +1,56 @@ +#include +#include +#include +#include +#include + +#include /* PATH_MAX*/ +#include "ini_rw.h" +#define main_c + + + +int main() +{ + char filename[] = "tests/test.ini", *buff1, *buff2, *buff3, *buff4, *buff5; + lci_data *ini=NULL, *tmp=NULL; + int len=0, elen=50; + FILE *fp; + + fp = fopen(filename, "rb"); + len = getFileMaxLineLen(fp); + fclose(fp); + + buff1 = malloc((len+3)*sizeof(char)); + buff2 = malloc((len+3)*sizeof(char)); + buff3 = malloc((len+3)*sizeof(char)); + buff4 = malloc((len+3)*sizeof(char)); + buff5 = malloc((elen+3)*sizeof(char)); + + + ini = iniReadOut(filename); + + + printf("\n"); + + + tmp = ini; + while(tmp != NULL){ + + snprintf(buff1, len+3, "'%s' %3ld",tmp->section, tmp->sectionLen); + snprintf(buff2, len+3, "'%s' %3ld",tmp->param, tmp->paramLen); + snprintf(buff3, len+3, "'%s' %3ld",tmp->value, tmp->valueLen); + snprintf(buff4, len+3, "'%s' %3ld",tmp->comment, tmp->commentLen); + snprintf(buff5, elen, "'%s' %3ld", tmp->errorMsg, tmp->errorMsgLen); + + + printf("LN: %ld,\tLL: %ld,\tSE: %*s, P: %*s, V: %*s, C: %*s, ER: %*s \n", tmp->lineNum, tmp->lineLen,len+6, buff1, len+6, buff2, len+6, buff3, len+6, buff4, elen, buff5); + + tmp=tmp->next; + } + + + + + return 0; +} -- cgit v1.2.3