From 4770c96d92a88dd93d5705221d02f3027bccddbc Mon Sep 17 00:00:00 2001 From: Epicalert Date: Thu, 4 Feb 2021 22:58:03 +0800 Subject: [PATCH] Add support for model files Model files are zip archives with a "model.toml" file at the root describing the model and all its textures. --- CMakeLists.txt | 6 +- models/test.fma | Bin 0 -> 28363 bytes models/test/face-eyes.png | Bin 9011 -> 0 bytes models/test/face-mouth-closed.png | Bin 5399 -> 0 bytes models/test/face-mouth-open.png | Bin 8072 -> 0 bytes models/test/head-base.png | Bin 19428 -> 0 bytes src/cv.cpp | 33 +- src/cv.hpp | 11 + src/graphics.cpp | 26 +- src/graphics.hpp | 8 +- src/main.cpp | 4 + src/model.cpp | 113 ++ src/model.hpp | 19 + src/modelpart.cpp | 57 +- src/modelpart.hpp | 28 +- src/toml.c | 2247 +++++++++++++++++++++++++++++ src/toml.h | 175 +++ src/tomlcpp.cpp | 334 +++++ src/tomlcpp.hpp | 128 ++ 19 files changed, 3140 insertions(+), 49 deletions(-) create mode 100644 models/test.fma delete mode 100644 models/test/face-eyes.png delete mode 100644 models/test/face-mouth-closed.png delete mode 100644 models/test/face-mouth-open.png delete mode 100644 models/test/head-base.png create mode 100644 src/model.cpp create mode 100644 src/model.hpp create mode 100644 src/toml.c create mode 100644 src/toml.h create mode 100644 src/tomlcpp.cpp create mode 100644 src/tomlcpp.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 97b373a..e6159de 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ cmake_minimum_required( VERSION 3.0 ) project( Facecam2D VERSION 0.1.0 ) +find_package( libzip REQUIRED ) find_package( OpenCV REQUIRED ) message (STATUS "Found OpenCV at: " ${OpenCV_INCLUDE_DIRS} ) find_package( OpenGL REQUIRED ) @@ -38,5 +39,8 @@ add_executable( fc2d src/cv.cpp src/paths.cpp src/args.cpp + src/model.cpp + src/toml.c + src/tomlcpp.cpp ) -target_link_libraries( fc2d ${OpenCV_LIBS} ${OPENGL_LIBRARIES} FreeGLUT::freeglut GLEW::glew ) +target_link_libraries( fc2d ${OpenCV_LIBS} ${OPENGL_LIBRARIES} FreeGLUT::freeglut GLEW::glew zip ) diff --git a/models/test.fma b/models/test.fma new file mode 100644 index 0000000000000000000000000000000000000000..a8c698aedc4ba3a9bfc3e557fb3e82b9509fa040 GIT binary patch literal 28363 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%0DsYz8ya(#QIjTx7 z(A_0UL`L*mox5~dHHvWb2NLbXG5J+?^P|p+**PYpdtx6w>zevXWI4L-?if;H$R6Xn zm~1;UXZ&d+kL_Fn8N$iXb@1`mq(>M5uP+GsNJqtmzCGGVXQ z9D(8Dxq+jjlz~$I6RL;ShKd6^*gtqj+dvd2=M~5pC?XPw{8L(&Kj|^cev$LICEA5g zN_SH>UPsNpA!0p;Rkm561n6O`VV|GN;_}fWH`PA5uIZYZbnY>VuqPo)NA7g7F1Lp< z6L(Z>+CVPGzUkJ`v7AFHyYu2| zpr%5)sYU3Dp1aIvJ!S}L==!;#@Y3WG?#&Z`uCl!g?ACmrQOgS(|>FBRY?| zWLeP+-lKhy{Zg1D-|f0Fi4&Ta}pLCep{X<<=L0M3cGB6lJL0{)Yc@KdSX%i zq@SoqIiZ}gn)^B4`RCg;T-Pdb|(HS9wx zN=Qb(-vaZ3L*?Tko~@9sZ-MQUjxx~{J|6Q93{fH{7jPw-!Y0>wtiw!|g5s9SscF(1 zkG{fLWvGk%n80Df^;pjsQu+g>UYGY@!)N>XlNK*Ur;_1iQKqm8k zeFD@|dj$NDySt=Rn{Lv%a*SVNT1K&~5L$k#Nz_Rih7{ecD{cdcr!PRH=0 zIFsr$X$qs$3hKFg?KmW-^EM4RzM2Zzt_S~#9G658xs~d*()C_EAEX{6GE{&crm8Ov zota-3O|#w{+A_(!@nFKTx`ula3H9`yO%fyv^`B>)`BtZVP%Gt5qXUuWClE&U(&1&k zedUE>kcla2#8{Lz6e1acG^3!1X9-7|7BX9NM>|2q4>0J)QMstZtlGL}M72aBI_~)2 zC^gk>SslPnnniyVTM1~=czPEL%rT)p!7vzE(hU{ve4Sm(-`=qZZgkCUGk0pC3wjZW zq_{cCUJe{eY2j6yWHJ&8{l9fE+(Gn04U|WA4i+aHIl}6_I~E<`vX=)EIM?tL+ymXY zHipSePgFqR+9HKz1lI#^yqr?T^?1n&;k?bcl1&hE5p`U4qTHK+&LlQKLQMrr^6culD`C_Y-=IXOgg zHhE=9*Kkx3jAp7f7#PzR+Za>SBJOM_7s9(dpjQrRgs7>m50%H(je zXZEgA*e3(vY^m+<4Ideui`zHkU z@8>rHzY+M2z;6WpR|tH^4qX+7Y7PLX@Q}kY|Eq0(@AF?pz>6t<0oni*{4cv9gaBLQmp;1{2%YEMnnJr diff --git a/models/test/face-mouth-closed.png b/models/test/face-mouth-closed.png deleted file mode 100644 index 175940ab79329383be651052cd5bbbeaacf15ea0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5399 zcmeH|doJvt{<;6`IeX4L=lss|`#sP1dEV#!yze=`j8jf_ zGTU}<0|3Zi?J+I@NI|bsV9RF6k=6840f1c&L8DJ$(P&guOk{9KSP%dQ@=5uY_Aiei zU-{x{HI?8;(uu!xTM*=w;(Bxyi*3r3JmQS%mJk$^y*dWDmqMGH^Ip+XvuYT!j7z8$ z*y#AwgDFjIfuYWmbl%8wjk#IN*&&z>^Nk5TRJxlua8i3TU|)moW*>Qsie-9$NANA< z{a7p!>AU=So@dZnV^O#oyF@q7f_;~taBJ5i$42X;wIQn1sV3CB(;~BUxb$HN&pAl`S zqXMt@MNVpMlln2`ct>USVyS~pP=iImXV)b2C)d3NZ_kihcYr#bk~ zGcj%6S?!I`v1|@FVTXY{WPEf38FE{r>2qHko1o;z#&YEm&66~2sHv`XWue)YV$?6#e4{Q+_1=RLT&lVLfv538SA zaRvg(N5&$PUl|V<3`9gXUSU>qC!!l-vwMkmNMWwdt^I@30(Vc1XCN6G=AMpWE=QPS z%)lGsyE;tY+!oC5Vp=T|nB>b{T=KxI(c;+b>VYqloTM> zPccqc;pyzEk`JbWq%|LsFUhsi_4SYgl=E_pJUvW|%4~QfNC>Q{_h#Zd8Xt;k?1R}? z*;+Ddi>`JcG#fs!G>k~fnCz$wCw>B%ftyx~4OG9}EbQv^VkdtCvrGAhT@ zmwI3~8>0l<&O5t=$q)m;IdkRArW zNd<6Dun7PZ^eG4a<`!2&g))#DboR8WY{Z}xkbpLHmuLJ{HIhvc&MB}qvip8wz$|9x7Y2)Sf*8F zQ-!)7d}BTbRd(2AKqV~-3B0VBjqaUVpbxo+b<(`^317IkR3OQ!KBxnA$Yj^?Gh?U} zJN7d*uSH=UM?dz~xKSUAKB6)BT8m}llEh%?%jNIq5JWn1DZr601Ryn

a3OTVjploM*>QFS=!bg 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 diff --git a/models/test/head-base.png b/models/test/head-base.png deleted file mode 100644 index 442f3fda64e68e501ddf1c6d1a110c2ac3199fd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19428 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 E0!M>s`~Uy| diff --git a/src/cv.cpp b/src/cv.cpp index 9722f72..ff0c7ee 100644 --- a/src/cv.cpp +++ b/src/cv.cpp @@ -6,6 +6,8 @@ #include #include #include +#include +#include cv::Ptr facemark; cv::CascadeClassifier haarFaceDetector; @@ -101,30 +103,29 @@ void cvFrame() { //send control information to graphics float faceSize = landmarks[biggestFace][14].x - landmarks[biggestFace][2].x; - updateModel( - //head position - glm::vec2( + + struct FaceData faceData; + faceData.positions[BIND_NULL] = glm::vec2(0.0f, 0.0f); + faceData.positions[BIND_HEAD] = glm::vec2( (landmarks[biggestFace][2].x + landmarks[biggestFace][14].x) / 2 * 2 / (float)frame.cols - 1, (landmarks[biggestFace][2].y + landmarks[biggestFace][14].y) / 2 * 2 / (float)frame.rows - 1 - ), - - //face position - glm::vec2( + ); + faceData.positions[BIND_FACE] = glm::vec2( landmarks[biggestFace][30].x * 2 / (float)frame.cols - 1, landmarks[biggestFace][30].y * 2 / (float)frame.rows - 1 - ), + ); + faceData.triggers[TRIGGER_NULL] = false; + faceData.triggers[TRIGGER_MOUTH_OPEN] = + (landmarks[biggestFace][66].y - landmarks[biggestFace][62].y) / faceSize > 0.04f; - //rotation - atanf((float)(landmarks[biggestFace][14].y - landmarks[biggestFace][2].y) / - (float)(landmarks[biggestFace][2].x - landmarks[biggestFace][14].x)), + faceData.headRotation = atanf( + (float)(landmarks[biggestFace][14].y - landmarks[biggestFace][2].y) / + (float)(landmarks[biggestFace][2].x - landmarks[biggestFace][14].x)); + faceData.scale = faceSize * 6 / (float)frame.cols; - //scale - faceSize * 6 / (float)frame.cols, - - //mouth open/closed state - (landmarks[biggestFace][66].y - landmarks[biggestFace][62].y) / faceSize > 0.04f); + updateModel(faceData); } } diff --git a/src/cv.hpp b/src/cv.hpp index adf1146..f47149a 100644 --- a/src/cv.hpp +++ b/src/cv.hpp @@ -1,6 +1,17 @@ #ifndef CV_HPP #define CV_HPP +#include +#include + +struct FaceData { + std::map positions; + std::map triggers; + + float headRotation; + float scale; +}; + void initCV(); void cvFrame(); diff --git a/src/graphics.cpp b/src/graphics.cpp index c72bdb5..cf62db6 100644 --- a/src/graphics.cpp +++ b/src/graphics.cpp @@ -12,21 +12,21 @@ #include #include +#include #include #include +#include GLuint shader; //standard shader program used for all elements GLuint transUniform; //location of the "transMatrix" transformation matrix uniform in the shader //parts of the model (see modelpart.hpp) -ModelPart parts[3]; +Model* model; void display () { glClear(GL_COLOR_BUFFER_BIT); - for (int i = 0; i < sizeof(parts)/sizeof(ModelPart); i++) { - parts[i].bindAndDraw(); - } + model->draw(); glutSwapBuffers(); } @@ -82,10 +82,7 @@ void initGraphics () { initShader(); - parts[0] = ModelPart(resolvePath("models/test/head-base.png").c_str(), transUniform); - parts[1] = ModelPart(resolvePath("models/test/face-eyes.png").c_str(), transUniform); - parts[2] = ModelPart(resolvePath("models/test/face-mouth-closed.png").c_str(), transUniform); - parts[2].addTexture(resolvePath("models/test/face-mouth-open.png").c_str(), 1); + model = new Model(resolvePath("models/test.fma").c_str()); //enable blending for alpha textures glEnable(GL_BLEND); @@ -100,12 +97,12 @@ void initGraphics () { std::cout << "graphics init complete" << std::endl; } -void initTexture (GLuint* texNum, const char* path) { +void initTexture (GLuint* texNum, unsigned char* buffer, size_t bufferLength) { glGenTextures(1, texNum); glBindTexture(GL_TEXTURE_2D, *texNum); int x, y, channels; - GLubyte* pixels = stbi_load(path, &x, &y, &channels, 4); + GLubyte* pixels = stbi_load_from_memory(buffer, bufferLength, &x, &y, &channels, 4); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); @@ -175,11 +172,8 @@ void printShaderCompileLog(GLuint shader) { std::cout << logBuffer << std::endl; } -void initModel () { - -} - -void updateModel(glm::vec2 headPos, glm::vec2 facePos, float rotation, float scale, bool mouthOpen) { +void updateModel(struct FaceData faceData) { + /* //calculate transforms parts[0].setTransform(headPos, rotation, scale); parts[1].setTransform(facePos, rotation, scale); @@ -187,6 +181,8 @@ void updateModel(glm::vec2 headPos, glm::vec2 facePos, float rotation, float sca //set mouth texture to open or closed parts[2].selectTexture(mouthOpen ? 1 : 0); + */ + model->updateTransforms(faceData); //tell FreeGLUT to schedule a screen update glutPostRedisplay(); diff --git a/src/graphics.hpp b/src/graphics.hpp index 18f539a..549e11b 100644 --- a/src/graphics.hpp +++ b/src/graphics.hpp @@ -9,18 +9,22 @@ #include +#include + +extern GLuint transUniform; + void initGraphics (); void graphicsFrame (); void initBuffers (GLuint* vaoNum); -void initTexture (GLuint* texNum, const char* path); +void initTexture (GLuint* texNum, unsigned char* buffer, size_t bufferLength); void initShader(); void printShaderCompileLog(GLuint shader); -void updateModel(glm::vec2 headPos, glm::vec2 facePos, float rotation, float scale, bool mouthOpen); +void updateModel(struct FaceData faceData); #endif diff --git a/src/main.cpp b/src/main.cpp index 67bad50..c28caf4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -17,8 +18,11 @@ int main (int argc, char** argv) { std::cout << "Default asset prefix: " << prefixDefault << std::endl; initGraphics(); + + //Model model(resolvePath("models/test.fma").c_str()); initCV(); + while (true) { cvFrame(); diff --git a/src/model.cpp b/src/model.cpp new file mode 100644 index 0000000..d8c03cf --- /dev/null +++ b/src/model.cpp @@ -0,0 +1,113 @@ +#include +#include +#include +#include //dynamically link tomlcpp if it becomes common in repositories +#include + +#define BUFFER_SIZE_MODEL_DESC 8192 // 8 KiB +#define BUFFER_SIZE_TEXTURE 16777220 // 16 MiB + +void textureFromArchive(zip_t* archive, const char* path, ModelPart* part, size_t slot, std::string triggerName) { + zip_file_t* textureFile = zip_fopen(archive, path, 0); + if (textureFile != NULL) { + unsigned char* textureBuffer = new unsigned char[BUFFER_SIZE_TEXTURE]; + size_t textureLength = zip_fread(textureFile, textureBuffer, BUFFER_SIZE_TEXTURE-1); + + part->addTexture(textureBuffer, textureLength, slot, triggerName); + + delete [] textureBuffer; + } else { + std::cerr << path << " does not exist in archive!" << std::endl; + } +} + + +Model::Model(const char* path) { + int zipError; + zip_t* archive = zip_open(path, ZIP_RDONLY, &zipError); + + if (!archive) { + std::cerr << "Model file " << path << " does not exist!" << std::endl; + return; + } + + // get model description file (model.toml) + zip_file_t* modelDescFile = zip_fopen(archive, "model.toml", 0); + char modelDescBuffer[BUFFER_SIZE_MODEL_DESC]; + size_t descLength = zip_fread(modelDescFile, modelDescBuffer, BUFFER_SIZE_MODEL_DESC-1); + modelDescBuffer[descLength] = 0; //null-terminate + + // parse model.toml + auto modelDesc = toml::parse(std::string(modelDescBuffer)); + if (!modelDesc.table) { + std::cerr << "cannot parse model.toml! " << std::endl << modelDesc.errmsg << std::endl; + } + + auto partsDescArray = modelDesc.table->getArray("part"); + + auto partsVec = *partsDescArray->getTableVector(); + + for (int i = 0; i < partsVec.size(); i++) { + ModelPart newPart; + + // position + auto bindResult = partsVec[i].getString("bind"); + if (!bindResult.first) { + std::cerr << "Part " << i << " does not define a bind!" << std::endl; + } else { + newPart.setBind(bindResult.second); + } + + auto parentResult = partsVec[i].getString("follow"); + auto factorResult = partsVec[i].getDouble("factor"); + + if (parentResult.first) { + newPart.setFollowTarget(parentResult.second); + + if (factorResult.first) { + newPart.setFollowFactor((float)factorResult.second); + } else { + newPart.setFollowFactor(1.0f); + } + } + + // texture + auto textureSingle = partsVec[i].getString("texture"); + + if (textureSingle.first) { + // only a single texture was defined + textureFromArchive(archive, textureSingle.second.c_str(), &newPart, 0, "null"); + } else { + auto textureArray = partsVec[i].getArray("textures"); + auto textureVec = *textureArray->getTableVector().get(); + + if (textureVec.size() < 1) { + std::cerr << "Part " << i << " does not define any textures!" << std::endl; + } else { + // a list of textures with triggers were defined + for (int j = 0; j < textureVec.size(); j++) { + auto fileResult = textureVec[j].getString("file"); + auto triggerResult = textureVec[j].getString("trigger"); + + if (fileResult.first) { + std::string trigger = triggerResult.first ? triggerResult.second : "null"; + textureFromArchive(archive, fileResult.second.c_str(), &newPart, j, trigger); + } + } + } + } + parts.push_back(newPart); + } +} + +void Model::draw() { + for (size_t i = 0; i < parts.size(); i++) { + parts[i].bindAndDraw(); + } +} + +void Model::updateTransforms(struct FaceData faceData) { + for (size_t i = 0; i < parts.size(); i++) { + parts[i].processFaceData(faceData); + } +} diff --git a/src/model.hpp b/src/model.hpp new file mode 100644 index 0000000..37cad51 --- /dev/null +++ b/src/model.hpp @@ -0,0 +1,19 @@ +#ifndef MODEL_HPP +#define MODEL_HPP + +#include +#include +#include + +class Model { + std::vector parts; + + public: + Model(const char* path); + + void draw(); + + void updateTransforms(struct FaceData faceData); +}; + +#endif diff --git a/src/modelpart.cpp b/src/modelpart.cpp index b5ea734..74f46d1 100644 --- a/src/modelpart.cpp +++ b/src/modelpart.cpp @@ -1,5 +1,4 @@ #include - #include #include @@ -8,36 +7,72 @@ #include -ModelPart::ModelPart() { -} +std::map bindStringToNum { + {"null", BIND_NULL}, + {"head", BIND_HEAD}, + {"face", BIND_FACE}, +}; -ModelPart::ModelPart(const char* texPath, GLuint transUniformNum) { +std::map triggerStringToNum { + {"null", TRIGGER_NULL}, + {"mouth-open", TRIGGER_MOUTH_OPEN}, +}; + +ModelPart::ModelPart() { //create vbo, ebo, vao initBuffers(&vao); - //create texture - initTexture(&tex[0], texPath); - - transUniform = transUniformNum; empty = false; } void ModelPart::bindAndDraw() { - if (empty) { return; } glBindVertexArray(vao); glBindTexture(GL_TEXTURE_2D, tex[texSelection]); glUniformMatrix4fv(transUniform, 1, GL_FALSE, glm::value_ptr(transMatrix)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); } +void ModelPart::setBind(std::string bindName) { + bind = bindStringToNum[bindName]; +} + +void ModelPart::setFollowTarget(std::string followTarget) { + follow = bindStringToNum[followTarget]; +} + +void ModelPart::setFollowFactor(float followFactor) { + factor = followFactor; +} + void ModelPart::setTransform(glm::vec2 position, float rotation, float scale) { transMatrix = glm::translate(glm::mat4(1.0f), glm::vec3(position.x, -position.y, 0.0f)); transMatrix = glm::rotate(transMatrix, rotation, glm::vec3(0.0f, 0.0f, 1.0f)); transMatrix = glm::scale(transMatrix, glm::vec3(scale, scale, scale)); } -void ModelPart::addTexture(const char* texPath, size_t slot) { - initTexture(&tex[slot], texPath); +void ModelPart::processFaceData(struct FaceData faceData) { + // calculate position + glm::vec2 bindPosition = faceData.positions[bind]; + glm::vec2 followPosition = faceData.positions[follow]; + glm::vec2 followDirection = followPosition - bindPosition; + glm::vec2 newPosition = bindPosition + (followDirection * factor); + + setTransform(newPosition, faceData.headRotation, faceData.scale); + + // change textures + selectTexture(0); // if none are triggered, use the first one + for (size_t i = 0; i < texCount; i++) { + if (faceData.triggers[texTriggers[i]]) { + selectTexture(i); + break; // if several textures are triggered, the first one defined in model.toml will get priority + } + } +} + +void ModelPart::addTexture(unsigned char* texBuffer, size_t texLength, size_t slot, std::string triggerName) { + initTexture(&tex[slot], texBuffer, texLength); + texTriggers[slot] = triggerStringToNum[triggerName]; + texCount++; } void ModelPart::selectTexture(size_t slot) { diff --git a/src/modelpart.hpp b/src/modelpart.hpp index ad62fea..90c9612 100644 --- a/src/modelpart.hpp +++ b/src/modelpart.hpp @@ -1,29 +1,49 @@ #ifndef MODELPART_HPP #define MODELPART_HPP +#include + #include +#include +#include + +#define BIND_NULL 0x00 +#define BIND_HEAD 0x01 +#define BIND_FACE 0x02 + +#define TRIGGER_NULL 0x00 +#define TRIGGER_MOUTH_OPEN 0x01 class ModelPart { - GLuint vao, transUniform; + GLuint vao; GLuint tex[16]; //support 16 textures to switch between + int texTriggers[16]; size_t texSelection = 0; + size_t texCount = 0; glm::mat4 transMatrix = glm::mat4(1.0f); + int bind = BIND_NULL; + int follow = BIND_NULL; + float factor = 0.0f; //default factor of 0 so part will not follow a null by default + bool empty = true; public: ModelPart(); - ModelPart(const char* texPath, GLuint transUniformNum); - void bindAndDraw(); + void setBind(std::string bindString); + void setFollowTarget(std::string followString); + void setFollowFactor(float followFactor); + void setTransform(glm::vec2 position, float rotation, float scale); - void addTexture(const char* texPath, size_t slot); + void processFaceData(struct FaceData faceData); + void addTexture(unsigned char* texBuffer, size_t texLength, size_t slot, std::string triggerName); void selectTexture(size_t slot); }; diff --git a/src/toml.c b/src/toml.c new file mode 100644 index 0000000..bb899d5 --- /dev/null +++ b/src/toml.c @@ -0,0 +1,2247 @@ +/* + + MIT License + + Copyright (c) 2017 - 2019 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +*/ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include "toml.h" + + +static void* (*ppmalloc)(size_t) = malloc; +static void (*ppfree)(void*) = free; + +void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)) +{ + if (xxmalloc) ppmalloc = xxmalloc; + if (xxfree) ppfree = xxfree; +} + + +#define MALLOC(a) ppmalloc(a) +#define FREE(a) ppfree(a) + +static void* CALLOC(size_t nmemb, size_t sz) +{ + int nb = sz * nmemb; + void* p = MALLOC(nb); + if (p) { + memset(p, 0, nb); + } + return p; +} + + +static char* STRDUP(const char* s) +{ + int len = strlen(s); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + +static char* STRNDUP(const char* s, size_t n) +{ + size_t len = strnlen(s, n); + char* p = MALLOC(len+1); + if (p) { + memcpy(p, s, len); + p[len] = 0; + } + return p; +} + + + +/** + * Convert a char in utf8 into UCS, and store it in *ret. + * Return #bytes consumed or -1 on failure. + */ +int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret) +{ + const unsigned char* buf = (const unsigned char*) orig; + unsigned i = *buf++; + int64_t v; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (0 == (i >> 7)) { + if (len < 1) return -1; + v = i; + return *ret = v, 1; + } + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (0x6 == (i >> 5)) { + if (len < 2) return -1; + v = i & 0x1f; + for (int j = 0; j < 1; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (0xE == (i >> 4)) { + if (len < 3) return -1; + v = i & 0x0F; + for (int j = 0; j < 2; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x1E == (i >> 3)) { + if (len < 4) return -1; + v = i & 0x07; + for (int j = 0; j < 3; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x3E == (i >> 2)) { + if (len < 5) return -1; + v = i & 0x03; + for (int j = 0; j < 4; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (0x7e == (i >> 1)) { + if (len < 6) return -1; + v = i & 0x01; + for (int j = 0; j < 5; j++) { + i = *buf++; + if (0x2 != (i >> 6)) return -1; + v = (v << 6) | (i & 0x3f); + } + return *ret = v, (const char*) buf - orig; + } + return -1; +} + + +/** + * Convert a UCS char to utf8 code, and return it in buf. + * Return #bytes used in buf to encode the char, or + * -1 on error. + */ +int toml_ucs_to_utf8(int64_t code, char buf[6]) +{ + /* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 */ + /* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well + * as 0xfffe and 0xffff (UCS noncharacters) should not appear in + * conforming UTF-8 streams. + */ + if (0xd800 <= code && code <= 0xdfff) return -1; + if (0xfffe <= code && code <= 0xffff) return -1; + + /* 0x00000000 - 0x0000007F: + 0xxxxxxx + */ + if (code < 0) return -1; + if (code <= 0x7F) { + buf[0] = (unsigned char) code; + return 1; + } + + /* 0x00000080 - 0x000007FF: + 110xxxxx 10xxxxxx + */ + if (code <= 0x000007FF) { + buf[0] = 0xc0 | (code >> 6); + buf[1] = 0x80 | (code & 0x3f); + return 2; + } + + /* 0x00000800 - 0x0000FFFF: + 1110xxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x0000FFFF) { + buf[0] = 0xe0 | (code >> 12); + buf[1] = 0x80 | ((code >> 6) & 0x3f); + buf[2] = 0x80 | (code & 0x3f); + return 3; + } + + /* 0x00010000 - 0x001FFFFF: + 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x001FFFFF) { + buf[0] = 0xf0 | (code >> 18); + buf[1] = 0x80 | ((code >> 12) & 0x3f); + buf[2] = 0x80 | ((code >> 6) & 0x3f); + buf[3] = 0x80 | (code & 0x3f); + return 4; + } + + /* 0x00200000 - 0x03FFFFFF: + 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x03FFFFFF) { + buf[0] = 0xf8 | (code >> 24); + buf[1] = 0x80 | ((code >> 18) & 0x3f); + buf[2] = 0x80 | ((code >> 12) & 0x3f); + buf[3] = 0x80 | ((code >> 6) & 0x3f); + buf[4] = 0x80 | (code & 0x3f); + return 5; + } + + /* 0x04000000 - 0x7FFFFFFF: + 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx + */ + if (code <= 0x7FFFFFFF) { + buf[0] = 0xfc | (code >> 30); + buf[1] = 0x80 | ((code >> 24) & 0x3f); + buf[2] = 0x80 | ((code >> 18) & 0x3f); + buf[3] = 0x80 | ((code >> 12) & 0x3f); + buf[4] = 0x80 | ((code >> 6) & 0x3f); + buf[5] = 0x80 | (code & 0x3f); + return 6; + } + + return -1; +} + +/* + * TOML has 3 data structures: value, array, table. + * Each of them can have identification key. + */ +typedef struct toml_keyval_t toml_keyval_t; +struct toml_keyval_t { + const char* key; /* key to this value */ + const char* val; /* the raw value */ +}; + + +struct toml_array_t { + const char* key; /* key to this array */ + int kind; /* element kind: 'v'alue, 'a'rray, or 't'able */ + int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp */ + + int nelem; /* number of elements */ + union { + char** val; + toml_array_t** arr; + toml_table_t** tab; + } u; +}; + + +struct toml_table_t { + const char* key; /* key to this table */ + bool implicit; /* table was created implicitly */ + + /* key-values in the table */ + int nkval; + toml_keyval_t** kval; + + /* arrays in the table */ + int narr; + toml_array_t** arr; + + /* tables in the table */ + int ntab; + toml_table_t** tab; +}; + + +static inline void xfree(const void* x) { if (x) FREE((void*)(intptr_t)x); } + + +enum tokentype_t { + INVALID, + DOT, + COMMA, + EQUAL, + LBRACE, + RBRACE, + NEWLINE, + LBRACKET, + RBRACKET, + STRING, +}; +typedef enum tokentype_t tokentype_t; + +typedef struct token_t token_t; +struct token_t { + tokentype_t tok; + int lineno; + char* ptr; /* points into context->start */ + int len; + int eof; +}; + + +typedef struct context_t context_t; +struct context_t { + char* start; + char* stop; + char* errbuf; + int errbufsz; + + token_t tok; + toml_table_t* root; + toml_table_t* curtab; + + struct { + int top; + char* key[10]; + token_t tok[10]; + } tpath; + +}; + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) +#define FLINE __FILE__ ":" TOSTRING(__LINE__) + +static int next_token(context_t* ctx, int dotisspecial); + +/* + Error reporting. Call when an error is detected. Always return -1. +*/ +static int e_outofmemory(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline); + return -1; +} + + +static int e_internal(context_t* ctx, const char* fline) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline); + return -1; +} + +static int e_syntax(context_t* ctx, int lineno, const char* msg) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg); + return -1; +} + +static int e_badkey(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno); + return -1; +} + +static int e_keyexists(context_t* ctx, int lineno) +{ + snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno); + return -1; +} + +static void* expand(void* p, int sz, int newsz) +{ + void* s = MALLOC(newsz); + if (!s) return 0; + + memcpy(s, p, sz); + FREE(p); + return s; +} + +static void** expand_ptrarr(void** p, int n) +{ + void** s = MALLOC((n+1) * sizeof(void*)); + if (!s) return 0; + + s[n] = 0; + memcpy(s, p, n * sizeof(void*)); + FREE(p); + return s; +} + + +static char* norm_lit_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + /* control characters other than tab is not allowed */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + } + + dst[off++] = 0; + return dst; +} + + + + +/* + * Convert src to raw unescaped utf-8 string. + * Returns NULL if error with errmsg in errbuf. + */ +static char* norm_basic_str(const char* src, int srclen, + int multiline, + char* errbuf, int errbufsz) +{ + char* dst = 0; /* will write to dst[] and return it */ + int max = 0; /* max size of dst[] */ + int off = 0; /* cur offset in dst[] */ + const char* sp = src; + const char* sq = src + srclen; + int ch; + + /* scan forward on src */ + for (;;) { + if (off >= max - 10) { /* have some slack for misc stuff */ + int newmax = max + 50; + char* x = expand(dst, max, newmax); + if (!x) { + xfree(dst); + snprintf(errbuf, errbufsz, "out of memory"); + return 0; + } + dst = x; + max = newmax; + } + + /* finished? */ + if (sp >= sq) break; + + ch = *sp++; + if (ch != '\\') { + /* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F, U+007F */ + if ((0 <= ch && ch <= 0x08) + || (0x0a <= ch && ch <= 0x1f) + || (ch == 0x7f)) { + if (! (multiline && (ch == '\r' || ch == '\n'))) { + xfree(dst); + snprintf(errbuf, errbufsz, "invalid char U+%04x", ch); + return 0; + } + } + + // a plain copy suffice + dst[off++] = ch; + continue; + } + + /* ch was backslash. we expect the escape char. */ + if (sp >= sq) { + snprintf(errbuf, errbufsz, "last backslash is invalid"); + xfree(dst); + return 0; + } + + /* for multi-line, we want to kill line-ending-backslash ... */ + if (multiline) { + + // if there is only whitespace after the backslash ... + if (sp[strspn(sp, " \t\r")] == '\n') { + /* skip all the following whitespaces */ + sp += strspn(sp, " \t\r\n"); + continue; + } + } + + /* get the escaped char */ + ch = *sp++; + switch (ch) { + case 'u': case 'U': + { + int64_t ucs = 0; + int nhex = (ch == 'u' ? 4 : 8); + for (int i = 0; i < nhex; i++) { + if (sp >= sq) { + snprintf(errbuf, errbufsz, "\\%c expects %d hex chars", ch, nhex); + xfree(dst); + return 0; + } + ch = *sp++; + int v = ('0' <= ch && ch <= '9') + ? ch - '0' + : (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1); + if (-1 == v) { + snprintf(errbuf, errbufsz, "invalid hex chars for \\u or \\U"); + xfree(dst); + return 0; + } + ucs = ucs * 16 + v; + } + int n = toml_ucs_to_utf8(ucs, &dst[off]); + if (-1 == n) { + snprintf(errbuf, errbufsz, "illegal ucs code in \\u or \\U"); + xfree(dst); + return 0; + } + off += n; + } + continue; + + case 'b': ch = '\b'; break; + case 't': ch = '\t'; break; + case 'n': ch = '\n'; break; + case 'f': ch = '\f'; break; + case 'r': ch = '\r'; break; + case '"': ch = '"'; break; + case '\\': ch = '\\'; break; + default: + snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch); + xfree(dst); + return 0; + } + + dst[off++] = ch; + } + + // Cap with NUL and return it. + dst[off++] = 0; + return dst; +} + + +/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */ +static char* normalize_key(context_t* ctx, token_t strtok) +{ + const char* sp = strtok.ptr; + const char* sq = strtok.ptr + strtok.len; + int lineno = strtok.lineno; + char* ret; + int ch = *sp; + char ebuf[80]; + + /* handle quoted string */ + if (ch == '\'' || ch == '\"') { + /* if ''' or """, take 3 chars off front and back. Else, take 1 char off. */ + int multiline = 0; + if (sp[1] == ch && sp[2] == ch) { + sp += 3, sq -= 3; + multiline = 1; + } + else + sp++, sq--; + + if (ch == '\'') { + /* for single quote, take it verbatim. */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + } else { + /* for double quote, we need to normalize */ + ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf)); + if (!ret) { + e_syntax(ctx, lineno, ebuf); + return 0; + } + } + + /* newlines are not allowed in keys */ + if (strchr(ret, '\n')) { + xfree(ret); + e_badkey(ctx, lineno); + return 0; + } + return ret; + } + + /* for bare-key allow only this regex: [A-Za-z0-9_-]+ */ + const char* xp; + for (xp = sp; xp != sq; xp++) { + int k = *xp; + if (isalnum(k)) continue; + if (k == '_' || k == '-') continue; + e_badkey(ctx, lineno); + return 0; + } + + /* dup and return it */ + if (! (ret = STRNDUP(sp, sq - sp))) { + e_outofmemory(ctx, FLINE); + return 0; + } + return ret; +} + + +/* + * Look up key in tab. Return 0 if not found, or + * 'v'alue, 'a'rray or 't'able depending on the element. + */ +static int check_key(toml_table_t* tab, const char* key, + toml_keyval_t** ret_val, + toml_array_t** ret_arr, + toml_table_t** ret_tab) +{ + int i; + void* dummy; + + if (!ret_tab) ret_tab = (toml_table_t**) &dummy; + if (!ret_arr) ret_arr = (toml_array_t**) &dummy; + if (!ret_val) ret_val = (toml_keyval_t**) &dummy; + + *ret_tab = 0; *ret_arr = 0; *ret_val = 0; + + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) { + *ret_val = tab->kval[i]; + return 'v'; + } + } + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) { + *ret_arr = tab->arr[i]; + return 'a'; + } + } + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) { + *ret_tab = tab->tab[i]; + return 't'; + } + } + return 0; +} + + +static int key_kind(toml_table_t* tab, const char* key) +{ + return check_key(tab, key, 0, 0, 0); +} + +/* Create a keyval in the table. + */ +static toml_keyval_t* create_keyval_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out. */ + toml_keyval_t* dest = 0; + if (key_kind(tab, newkey)) { + xfree(newkey); + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new entry */ + int n = tab->nkval; + toml_keyval_t** base; + if (0 == (base = (toml_keyval_t**) expand_ptrarr((void**)tab->kval, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->kval = base; + + if (0 == (base[n] = (toml_keyval_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->kval[tab->nkval++]; + + /* save the key in the new value struct */ + dest->key = newkey; + return dest; +} + + +/* Create a table in the table. + */ +static toml_table_t* create_keytable_in_table(context_t* ctx, toml_table_t* tab, token_t keytok) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + toml_table_t* dest = 0; + if (check_key(tab, newkey, 0, 0, &dest)) { + xfree(newkey); /* don't need this anymore */ + + /* special case: if table exists, but was created implicitly ... */ + if (dest && dest->implicit) { + /* we make it explicit now, and simply return it. */ + dest->implicit = false; + return dest; + } + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* create a new table entry */ + int n = tab->ntab; + toml_table_t** base; + if (0 == (base = (toml_table_t**) expand_ptrarr((void**)tab->tab, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + dest = tab->tab[tab->ntab++]; + + /* save the key in the new table struct */ + dest->key = newkey; + return dest; +} + + +/* Create an array in the table. + */ +static toml_array_t* create_keyarray_in_table(context_t* ctx, + toml_table_t* tab, + token_t keytok, + char kind) +{ + /* first, normalize the key to be used for lookup. + * remember to free it if we error out. + */ + char* newkey = normalize_key(ctx, keytok); + if (!newkey) return 0; + + /* if key exists: error out */ + if (key_kind(tab, newkey)) { + xfree(newkey); /* don't need this anymore */ + e_keyexists(ctx, keytok.lineno); + return 0; + } + + /* make a new array entry */ + int n = tab->narr; + toml_array_t** base; + if (0 == (base = (toml_array_t**) expand_ptrarr((void**)tab->arr, n))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + tab->arr = base; + + if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) { + xfree(newkey); + e_outofmemory(ctx, FLINE); + return 0; + } + toml_array_t* dest = tab->arr[tab->narr++]; + + /* save the key in the new array struct */ + dest->key = newkey; + dest->kind = kind; + return dest; +} + +/* Create an array in an array + */ +static toml_array_t* create_array_in_array(context_t* ctx, + toml_array_t* parent) +{ + const int n = parent->nelem; + toml_array_t** base; + if (0 == (base = (toml_array_t**) expand_ptrarr((void**)parent->u.arr, n))) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->u.arr = base; + parent->nelem++; + + if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) { + e_outofmemory(ctx, FLINE); + return 0; + } + + return parent->u.arr[n]; +} + +/* Create a table in an array + */ +static toml_table_t* create_table_in_array(context_t* ctx, + toml_array_t* parent) +{ + int n = parent->nelem; + toml_table_t** base; + if (0 == (base = (toml_table_t**) expand_ptrarr((void**)parent->u.tab, n))) { + e_outofmemory(ctx, FLINE); + return 0; + } + parent->u.tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) { + e_outofmemory(ctx, FLINE); + return 0; + } + + return parent->u.tab[parent->nelem++]; +} + + +static int skip_newlines(context_t* ctx, int isdotspecial) +{ + while (ctx->tok.tok == NEWLINE) { + if (next_token(ctx, isdotspecial)) return -1; + if (ctx->tok.eof) break; + } + return 0; +} + + +static int parse_keyval(context_t* ctx, toml_table_t* tab); + +static inline int eat_token(context_t* ctx, tokentype_t typ, int isdotspecial, const char* fline) +{ + if (ctx->tok.tok != typ) + return e_internal(ctx, fline); + + if (next_token(ctx, isdotspecial)) + return -1; + + return 0; +} + + + +/* We are at '{ ... }'. + * Parse the table. + */ +static int parse_table(context_t* ctx, toml_table_t* tab) +{ + if (eat_token(ctx, LBRACE, 1, FLINE)) + return -1; + + for (;;) { + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* until } */ + if (ctx->tok.tok == RBRACE) + break; + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, ctx->tok.lineno, "expect a string"); + + if (parse_keyval(ctx, tab)) + return -1; + + if (ctx->tok.tok == NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table"); + + /* on comma, continue to scan for next keyval */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 1, FLINE)) + return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACE, 1, FLINE)) + return -1; + return 0; +} + +static int valtype(const char* val) +{ + toml_timestamp_t ts; + if (*val == '\'' || *val == '"') return 's'; + if (0 == toml_rtob(val, 0)) return 'b'; + if (0 == toml_rtoi(val, 0)) return 'i'; + if (0 == toml_rtod(val, 0)) return 'd'; + if (0 == toml_rtots(val, &ts)) { + if (ts.year && ts.hour) return 'T'; /* timestamp */ + if (ts.year) return 'D'; /* date */ + return 't'; /* time */ + } + return 'u'; /* unknown */ +} + + +/* We are at '[...]' */ +static int parse_array(context_t* ctx, toml_array_t* arr) +{ + if (eat_token(ctx, LBRACKET, 0, FLINE)) return -1; + + for (;;) { + if (skip_newlines(ctx, 0)) return -1; + + /* until ] */ + if (ctx->tok.tok == RBRACKET) break; + + switch (ctx->tok.tok) { + case STRING: + { + char* val = ctx->tok.ptr; + int vlen = ctx->tok.len; + + /* set array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 'v'; + /* check array kind */ + if (arr->kind != 'v') + return e_syntax(ctx, ctx->tok.lineno, "a string array can only contain strings"); + + /* make a new value in array */ + char** tmp = (char**) expand_ptrarr((void**)arr->u.val, arr->nelem); + if (!tmp) + return e_outofmemory(ctx, FLINE); + + arr->u.val = tmp; + if (! (val = STRNDUP(val, vlen))) + return e_outofmemory(ctx, FLINE); + + arr->u.val[arr->nelem++] = val; + + /* set array type if this is the first entry, or check that the types matched. */ + if (arr->nelem == 1) + arr->type = valtype(arr->u.val[0]); + else if (arr->type != valtype(val)) { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of values"); + } + + if (eat_token(ctx, STRING, 0, FLINE)) return -1; + break; + } + + case LBRACKET: + { /* [ [array], [array] ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 'a'; + /* check array kind */ + if (arr->kind != 'a') { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of arrays"); + } + toml_array_t* subarr = create_array_in_array(ctx, arr); + if (!subarr) return -1; + if (parse_array(ctx, subarr)) return -1; + break; + } + + case LBRACE: + { /* [ {table}, {table} ... ] */ + /* set the array kind if this will be the first entry */ + if (arr->kind == 0) arr->kind = 't'; + /* check array kind */ + if (arr->kind != 't') { + return e_syntax(ctx, ctx->tok.lineno, + "array type mismatch while processing array of tables"); + } + toml_table_t* subtab = create_table_in_array(ctx, arr); + if (!subtab) return -1; + if (parse_table(ctx, subtab)) return -1; + break; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + + if (skip_newlines(ctx, 0)) return -1; + + /* on comma, continue to scan for next element */ + if (ctx->tok.tok == COMMA) { + if (eat_token(ctx, COMMA, 0, FLINE)) return -1; + continue; + } + break; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + return 0; +} + + +/* handle lines like these: + key = "value" + key = [ array ] + key = { table } +*/ +static int parse_keyval(context_t* ctx, toml_table_t* tab) +{ + token_t key = ctx->tok; + if (eat_token(ctx, STRING, 1, FLINE)) return -1; + + if (ctx->tok.tok == DOT) { + /* handle inline dotted key. + e.g. + physical.color = "orange" + physical.shape = "round" + */ + toml_table_t* subtab = 0; + { + char* subtabstr = normalize_key(ctx, key); + subtab = toml_table_in(tab, subtabstr); + xfree(subtabstr); + } + if (!subtab) { + subtab = create_keytable_in_table(ctx, tab, key); + if (!subtab) return -1; + } + if (next_token(ctx, 1)) return -1; + if (parse_keyval(ctx, subtab)) return -1; + return 0; + } + + if (ctx->tok.tok != EQUAL) { + return e_syntax(ctx, ctx->tok.lineno, "missing ="); + } + + if (next_token(ctx, 0)) return -1; + + switch (ctx->tok.tok) { + case STRING: + { /* key = "value" */ + toml_keyval_t* keyval = create_keyval_in_table(ctx, tab, key); + if (!keyval) return -1; + token_t val = ctx->tok; + + assert(keyval->val == 0); + if (! (keyval->val = STRNDUP(val.ptr, val.len))) + return e_outofmemory(ctx, FLINE); + + if (next_token(ctx, 1)) return -1; + + return 0; + } + + case LBRACKET: + { /* key = [ array ] */ + toml_array_t* arr = create_keyarray_in_table(ctx, tab, key, 0); + if (!arr) return -1; + if (parse_array(ctx, arr)) return -1; + return 0; + } + + case LBRACE: + { /* key = { table } */ + toml_table_t* nxttab = create_keytable_in_table(ctx, tab, key); + if (!nxttab) return -1; + if (parse_table(ctx, nxttab)) return -1; + return 0; + } + + default: + return e_syntax(ctx, ctx->tok.lineno, "syntax error"); + } + return 0; +} + + +typedef struct tabpath_t tabpath_t; +struct tabpath_t { + int cnt; + token_t key[10]; +}; + +/* at [x.y.z] or [[x.y.z]] + * Scan forward and fill tabpath until it enters ] or ]] + * There will be at least one entry on return. + */ +static int fill_tabpath(context_t* ctx) +{ + int lineno = ctx->tok.lineno; + int i; + + /* clear tpath */ + for (i = 0; i < ctx->tpath.top; i++) { + char** p = &ctx->tpath.key[i]; + xfree(*p); + *p = 0; + } + ctx->tpath.top = 0; + + for (;;) { + if (ctx->tpath.top >= 10) + return e_syntax(ctx, lineno, "table path is too deep; max allowed is 10."); + + if (ctx->tok.tok != STRING) + return e_syntax(ctx, lineno, "invalid or missing key"); + + char* key = normalize_key(ctx, ctx->tok); + if (!key) return -1; + ctx->tpath.tok[ctx->tpath.top] = ctx->tok; + ctx->tpath.key[ctx->tpath.top] = key; + ctx->tpath.top++; + + if (next_token(ctx, 1)) return -1; + + if (ctx->tok.tok == RBRACKET) break; + + if (ctx->tok.tok != DOT) + return e_syntax(ctx, lineno, "invalid key"); + + if (next_token(ctx, 1)) return -1; + } + + if (ctx->tpath.top <= 0) + return e_syntax(ctx, lineno, "empty table selector"); + + return 0; +} + + +/* Walk tabpath from the root, and create new tables on the way. + * Sets ctx->curtab to the final table. + */ +static int walk_tabpath(context_t* ctx) +{ + /* start from root */ + toml_table_t* curtab = ctx->root; + + for (int i = 0; i < ctx->tpath.top; i++) { + const char* key = ctx->tpath.key[i]; + + toml_keyval_t* nextval = 0; + toml_array_t* nextarr = 0; + toml_table_t* nexttab = 0; + switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) { + case 't': + /* found a table. nexttab is where we will go next. */ + break; + + case 'a': + /* found an array. nexttab is the last table in the array. */ + if (nextarr->kind != 't') + return e_internal(ctx, FLINE); + + if (nextarr->nelem == 0) + return e_internal(ctx, FLINE); + + nexttab = nextarr->u.tab[nextarr->nelem-1]; + break; + + case 'v': + return e_keyexists(ctx, ctx->tpath.tok[i].lineno); + + default: + { /* Not found. Let's create an implicit table. */ + int n = curtab->ntab; + toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)curtab->tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + curtab->tab = base; + + if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP(key))) + return e_outofmemory(ctx, FLINE); + + nexttab = curtab->tab[curtab->ntab++]; + + /* tabs created by walk_tabpath are considered implicit */ + nexttab->implicit = true; + } + break; + } + + /* switch to next tab */ + curtab = nexttab; + } + + /* save it */ + ctx->curtab = curtab; + + return 0; +} + + +/* handle lines like [x.y.z] or [[x.y.z]] */ +static int parse_select(context_t* ctx) +{ + assert(ctx->tok.tok == LBRACKET); + + /* true if [[ */ + int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '['); + /* need to detect '[[' on our own because next_token() will skip whitespace, + and '[ [' would be taken as '[[', which is wrong. */ + + /* eat [ or [[ */ + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + if (llb) { + assert(ctx->tok.tok == LBRACKET); + if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1; + } + + if (fill_tabpath(ctx)) return -1; + + /* For [x.y.z] or [[x.y.z]], remove z from tpath. + */ + token_t z = ctx->tpath.tok[ctx->tpath.top-1]; + xfree(ctx->tpath.key[ctx->tpath.top-1]); + ctx->tpath.top--; + + /* set up ctx->curtab */ + if (walk_tabpath(ctx)) return -1; + + if (! llb) { + /* [x.y.z] -> create z = {} in x.y */ + toml_table_t* curtab = create_keytable_in_table(ctx, ctx->curtab, z); + if (!curtab) return -1; + ctx->curtab = curtab; + } else { + /* [[x.y.z]] -> create z = [] in x.y */ + toml_array_t* arr = 0; + { + char* zstr = normalize_key(ctx, z); + if (!zstr) return -1; + arr = toml_array_in(ctx->curtab, zstr); + xfree(zstr); + } + if (!arr) { + arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't'); + if (!arr) return -1; + } + if (arr->kind != 't') + return e_syntax(ctx, z.lineno, "array mismatch"); + + /* add to z[] */ + toml_table_t* dest; + { + int n = arr->nelem; + toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)arr->u.tab, n); + if (0 == base) + return e_outofmemory(ctx, FLINE); + + arr->u.tab = base; + + if (0 == (base[n] = CALLOC(1, sizeof(*base[n])))) + return e_outofmemory(ctx, FLINE); + + if (0 == (base[n]->key = STRDUP("__anon__"))) + return e_outofmemory(ctx, FLINE); + + dest = arr->u.tab[arr->nelem++]; + } + + ctx->curtab = dest; + } + + if (ctx->tok.tok != RBRACKET) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]"); + } + if (llb) { + if (! (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) { + return e_syntax(ctx, ctx->tok.lineno, "expects ]]"); + } + if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1; + } + + if (eat_token(ctx, RBRACKET, 1, FLINE)) + return -1; + + if (ctx->tok.tok != NEWLINE) + return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]"); + + return 0; +} + + + + +toml_table_t* toml_parse(char* conf, + char* errbuf, + int errbufsz) +{ + context_t ctx; + + // clear errbuf + if (errbufsz <= 0) errbufsz = 0; + if (errbufsz > 0) errbuf[0] = 0; + + // init context + memset(&ctx, 0, sizeof(ctx)); + ctx.start = conf; + ctx.stop = ctx.start + strlen(conf); + ctx.errbuf = errbuf; + ctx.errbufsz = errbufsz; + + // start with an artificial newline of length 0 + ctx.tok.tok = NEWLINE; + ctx.tok.lineno = 1; + ctx.tok.ptr = conf; + ctx.tok.len = 0; + + // make a root table + if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) { + e_outofmemory(&ctx, FLINE); + // Do not goto fail, root table not set up yet + return 0; + } + + // set root as default table + ctx.curtab = ctx.root; + + /* Scan forward until EOF */ + for (token_t tok = ctx.tok; ! tok.eof ; tok = ctx.tok) { + switch (tok.tok) { + + case NEWLINE: + if (next_token(&ctx, 1)) goto fail; + break; + + case STRING: + if (parse_keyval(&ctx, ctx.curtab)) goto fail; + + if (ctx.tok.tok != NEWLINE) { + e_syntax(&ctx, ctx.tok.lineno, "extra chars after value"); + goto fail; + } + + if (eat_token(&ctx, NEWLINE, 1, FLINE)) goto fail; + break; + + case LBRACKET: /* [ x.y.z ] or [[ x.y.z ]] */ + if (parse_select(&ctx)) goto fail; + break; + + default: + e_syntax(&ctx, tok.lineno, "syntax error"); + goto fail; + } + } + + /* success */ + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + return ctx.root; + +fail: + // Something bad has happened. Free resources and return error. + for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]); + toml_free(ctx.root); + return 0; +} + + +toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz) +{ + int bufsz = 0; + char* buf = 0; + int off = 0; + + /* read from fp into buf */ + while (! feof(fp)) { + + if (off == bufsz) { + int xsz = bufsz + 1000; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + + errno = 0; + int n = fread(buf + off, 1, bufsz - off, fp); + if (ferror(fp)) { + snprintf(errbuf, errbufsz, "%s", + errno ? strerror(errno) : "Error reading file"); + xfree(buf); + return 0; + } + off += n; + } + + /* tag on a NUL to cap the string */ + if (off == bufsz) { + int xsz = bufsz + 1; + char* x = expand(buf, bufsz, xsz); + if (!x) { + snprintf(errbuf, errbufsz, "out of memory"); + xfree(buf); + return 0; + } + buf = x; + bufsz = xsz; + } + buf[off] = 0; + + /* parse it, cleanup and finish */ + toml_table_t* ret = toml_parse(buf, errbuf, errbufsz); + xfree(buf); + return ret; +} + + +static void xfree_kval(toml_keyval_t* p) +{ + if (!p) return; + xfree(p->key); + xfree(p->val); + xfree(p); +} + +static void xfree_tab(toml_table_t* p); + +static void xfree_arr(toml_array_t* p) +{ + if (!p) return; + + xfree(p->key); + switch (p->kind) { + case 'v': + for (int i = 0; i < p->nelem; i++) xfree(p->u.val[i]); + xfree(p->u.val); + break; + + case 'a': + for (int i = 0; i < p->nelem; i++) xfree_arr(p->u.arr[i]); + xfree(p->u.arr); + break; + + case 't': + for (int i = 0; i < p->nelem; i++) xfree_tab(p->u.tab[i]); + xfree(p->u.tab); + break; + } + + xfree(p); +} + + +static void xfree_tab(toml_table_t* p) +{ + int i; + + if (!p) return; + + xfree(p->key); + + for (i = 0; i < p->nkval; i++) xfree_kval(p->kval[i]); + xfree(p->kval); + + for (i = 0; i < p->narr; i++) xfree_arr(p->arr[i]); + xfree(p->arr); + + for (i = 0; i < p->ntab; i++) xfree_tab(p->tab[i]); + xfree(p->tab); + + xfree(p); +} + + +void toml_free(toml_table_t* tab) +{ + xfree_tab(tab); +} + + +static void set_token(context_t* ctx, tokentype_t tok, int lineno, char* ptr, int len) +{ + token_t t; + t.tok = tok; + t.lineno = lineno; + t.ptr = ptr; + t.len = len; + t.eof = 0; + ctx->tok = t; +} + +static void set_eof(context_t* ctx, int lineno) +{ + set_token(ctx, NEWLINE, lineno, ctx->stop, 0); + ctx->tok.eof = 1; +} + + +/* Scan p for n digits compositing entirely of [0-9] */ +static int scan_digits(const char* p, int n) +{ + int ret = 0; + for ( ; n > 0 && isdigit(*p); n--, p++) { + ret = 10 * ret + (*p - '0'); + } + return n ? -1 : ret; +} + +static int scan_date(const char* p, int* YY, int* MM, int* DD) +{ + int year, month, day; + year = scan_digits(p, 4); + month = (year >= 0 && p[4] == '-') ? scan_digits(p+5, 2) : -1; + day = (month >= 0 && p[7] == '-') ? scan_digits(p+8, 2) : -1; + if (YY) *YY = year; + if (MM) *MM = month; + if (DD) *DD = day; + return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1; +} + +static int scan_time(const char* p, int* hh, int* mm, int* ss) +{ + int hour, minute, second; + hour = scan_digits(p, 2); + minute = (hour >= 0 && p[2] == ':') ? scan_digits(p+3, 2) : -1; + second = (minute >= 0 && p[5] == ':') ? scan_digits(p+6, 2) : -1; + if (hh) *hh = hour; + if (mm) *mm = minute; + if (ss) *ss = second; + return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1; +} + + +static int scan_string(context_t* ctx, char* p, int lineno, int dotisspecial) +{ + char* orig = p; + if (0 == strncmp(p, "'''", 3)) { + p = strstr(p + 3, "'''"); + if (0 == p) { + return e_syntax(ctx, lineno, "unterminated triple-s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 3 - orig); + return 0; + } + + if (0 == strncmp(p, "\"\"\"", 3)) { + int hexreq = 0; /* #hex required */ + int escape = 0; + int qcnt = 0; /* count quote */ + for (p += 3; *p && qcnt < 3; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + if (p[strspn(p, " \t\r")] == '\n') continue; /* allow for line ending backslash */ + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + qcnt = (*p == '"') ? qcnt + 1 : 0; + } + if (qcnt != 3) { + return e_syntax(ctx, lineno, "unterminated triple-quote"); + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + if ('\'' == *p) { + for (p++; *p && *p != '\n' && *p != '\''; p++); + if (*p != '\'') { + return e_syntax(ctx, lineno, "unterminated s-quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + if ('\"' == *p) { + int hexreq = 0; /* #hex required */ + int escape = 0; + for (p++; *p; p++) { + if (escape) { + escape = 0; + if (strchr("btnfr\"\\", *p)) continue; + if (*p == 'u') { hexreq = 4; continue; } + if (*p == 'U') { hexreq = 8; continue; } + return e_syntax(ctx, lineno, "bad escape char"); + } + if (hexreq) { + hexreq--; + if (strchr("0123456789ABCDEF", *p)) continue; + return e_syntax(ctx, lineno, "expect hex char"); + } + if (*p == '\\') { escape = 1; continue; } + if (*p == '\n') break; + if (*p == '"') break; + } + if (*p != '"') { + return e_syntax(ctx, lineno, "unterminated quote"); + } + + set_token(ctx, STRING, lineno, orig, p + 1 - orig); + return 0; + } + + /* check for timestamp without quotes */ + if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) { + // forward thru the timestamp + for ( ; strchr("0123456789.:+-T Z", toupper(*p)); p++); + // squeeze out any spaces at end of string + for ( ; p[-1] == ' '; p--); + // tokenize + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; + } + + /* literals */ + for ( ; *p && *p != '\n'; p++) { + int ch = *p; + if (ch == '.' && dotisspecial) break; + if ('A' <= ch && ch <= 'Z') continue; + if ('a' <= ch && ch <= 'z') continue; + if (strchr("0123456789+-_.", ch)) continue; + break; + } + + set_token(ctx, STRING, lineno, orig, p - orig); + return 0; +} + + +static int next_token(context_t* ctx, int dotisspecial) +{ + int lineno = ctx->tok.lineno; + char* p = ctx->tok.ptr; + int i; + + /* eat this tok */ + for (i = 0; i < ctx->tok.len; i++) { + if (*p++ == '\n') + lineno++; + } + + /* make next tok */ + while (p < ctx->stop) { + /* skip comment. stop just before the \n. */ + if (*p == '#') { + for (p++; p < ctx->stop && *p != '\n'; p++); + continue; + } + + if (dotisspecial && *p == '.') { + set_token(ctx, DOT, lineno, p, 1); + return 0; + } + + switch (*p) { + case ',': set_token(ctx, COMMA, lineno, p, 1); return 0; + case '=': set_token(ctx, EQUAL, lineno, p, 1); return 0; + case '{': set_token(ctx, LBRACE, lineno, p, 1); return 0; + case '}': set_token(ctx, RBRACE, lineno, p, 1); return 0; + case '[': set_token(ctx, LBRACKET, lineno, p, 1); return 0; + case ']': set_token(ctx, RBRACKET, lineno, p, 1); return 0; + case '\n': set_token(ctx, NEWLINE, lineno, p, 1); return 0; + case '\r': case ' ': case '\t': + /* ignore white spaces */ + p++; + continue; + } + + return scan_string(ctx, p, lineno, dotisspecial); + } + + set_eof(ctx, lineno); + return 0; +} + + +const char* toml_key_in(const toml_table_t* tab, int keyidx) +{ + if (keyidx < tab->nkval) return tab->kval[keyidx]->key; + + keyidx -= tab->nkval; + if (keyidx < tab->narr) return tab->arr[keyidx]->key; + + keyidx -= tab->narr; + if (keyidx < tab->ntab) return tab->tab[keyidx]->key; + + return 0; +} + +toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->nkval; i++) { + if (0 == strcmp(key, tab->kval[i]->key)) + return tab->kval[i]->val; + } + return 0; +} + +toml_array_t* toml_array_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->narr; i++) { + if (0 == strcmp(key, tab->arr[i]->key)) + return tab->arr[i]; + } + return 0; +} + + +toml_table_t* toml_table_in(const toml_table_t* tab, const char* key) +{ + int i; + for (i = 0; i < tab->ntab; i++) { + if (0 == strcmp(key, tab->tab[i]->key)) + return tab->tab[i]; + } + return 0; +} + +toml_raw_t toml_raw_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 'v') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.val[idx]; +} + +char toml_array_kind(const toml_array_t* arr) +{ + return arr->kind; +} + +char toml_array_type(const toml_array_t* arr) +{ + if (arr->kind != 'v') + return 0; + + if (arr->nelem == 0) + return 0; + + return arr->type; +} + + +int toml_array_nelem(const toml_array_t* arr) +{ + return arr->nelem; +} + +const char* toml_array_key(const toml_array_t* arr) +{ + return arr ? arr->key : (const char*) NULL; +} + +int toml_table_nkval(const toml_table_t* tab) +{ + return tab->nkval; +} + +int toml_table_narr(const toml_table_t* tab) +{ + return tab->narr; +} + +int toml_table_ntab(const toml_table_t* tab) +{ + return tab->ntab; +} + +const char* toml_table_key(const toml_table_t* tab) +{ + return tab ? tab->key : (const char*) NULL; +} + +toml_array_t* toml_array_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 'a') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.arr[idx]; +} + +toml_table_t* toml_table_at(const toml_array_t* arr, int idx) +{ + if (arr->kind != 't') + return 0; + if (! (0 <= idx && idx < arr->nelem)) + return 0; + return arr->u.tab[idx]; +} + + +int toml_rtots(toml_raw_t src_, toml_timestamp_t* ret) +{ + if (! src_) return -1; + + const char* p = src_; + int must_parse_time = 0; + + memset(ret, 0, sizeof(*ret)); + + int* year = &ret->__buffer.year; + int* month = &ret->__buffer.month; + int* day = &ret->__buffer.day; + int* hour = &ret->__buffer.hour; + int* minute = &ret->__buffer.minute; + int* second = &ret->__buffer.second; + int* millisec = &ret->__buffer.millisec; + + /* parse date YYYY-MM-DD */ + if (0 == scan_date(p, year, month, day)) { + ret->year = year; + ret->month = month; + ret->day = day; + + p += 10; + if (*p) { + // parse the T or space separator + if (*p != 'T' && *p != ' ') return -1; + must_parse_time = 1; + p++; + } + } + + /* parse time HH:MM:SS */ + if (0 == scan_time(p, hour, minute, second)) { + ret->hour = hour; + ret->minute = minute; + ret->second = second; + + /* optionally, parse millisec */ + p += 8; + if (*p == '.') { + char* qq; + p++; + errno = 0; + *millisec = strtol(p, &qq, 0); + if (errno) { + return -1; + } + while (*millisec > 999) { + *millisec /= 10; + } + + ret->millisec = millisec; + p = qq; + } + + if (*p) { + /* parse and copy Z */ + char* z = ret->__buffer.z; + ret->z = z; + if (*p == 'Z' || *p == 'z') { + *z++ = 'Z'; p++; + *z = 0; + + } else if (*p == '+' || *p == '-') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + + if (*p == ':') { + *z++ = *p++; + + if (! (isdigit(p[0]) && isdigit(p[1]))) return -1; + *z++ = *p++; + *z++ = *p++; + } + + *z = 0; + } + } + } + if (*p != 0) + return -1; + + if (must_parse_time && !ret->hour) + return -1; + + return 0; +} + + +/* Raw to boolean */ +int toml_rtob(toml_raw_t src, int* ret_) +{ + if (!src) return -1; + int dummy; + int* ret = ret_ ? ret_ : &dummy; + + if (0 == strcmp(src, "true")) { + *ret = 1; + return 0; + } + if (0 == strcmp(src, "false")) { + *ret = 0; + return 0; + } + return -1; +} + + +/* Raw to integer */ +int toml_rtoi(toml_raw_t src, int64_t* ret_) +{ + if (!src) return -1; + + char buf[100]; + char* p = buf; + char* q = p + sizeof(buf); + const char* s = src; + int base = 0; + int64_t dummy; + int64_t* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_100 */ + if (s[0] == '_') + return -1; + + /* if 0 ... */ + if ('0' == s[0]) { + switch (s[1]) { + case 'x': base = 16; s += 2; break; + case 'o': base = 8; s += 2; break; + case 'b': base = 2; s += 2; break; + case '\0': return *ret = 0, 0; + default: + /* ensure no other digits after it */ + if (s[1]) return -1; + } + } + + /* just strip underscores and pass to strtoll */ + while (*s && p < q) { + int ch = *s++; + switch (ch) { + case '_': + // disallow '__' + if (s[0] == '_') return -1; + continue; /* skip _ */ + default: + break; + } + *p++ = ch; + } + if (*s || p == q) return -1; + + /* last char cannot be '_' */ + if (s[-1] == '_') return -1; + + /* cap with NUL */ + *p = 0; + + /* Run strtoll on buf to get the integer */ + char* endp; + errno = 0; + *ret = strtoll(buf, &endp, base); + return (errno || *endp) ? -1 : 0; +} + + +int toml_rtod_ex(toml_raw_t src, double* ret_, char* buf, int buflen) +{ + if (!src) return -1; + + char* p = buf; + char* q = p + buflen; + const char* s = src; + double dummy; + double* ret = ret_ ? ret_ : &dummy; + + + /* allow +/- */ + if (s[0] == '+' || s[0] == '-') + *p++ = *s++; + + /* disallow +_1.00 */ + if (s[0] == '_') + return -1; + + /* disallow +.99 */ + if (s[0] == '.') + return -1; + + /* zero must be followed by . or 'e', or NUL */ + if (s[0] == '0' && s[1] && !strchr("eE.", s[1])) + return -1; + + /* just strip underscores and pass to strtod */ + while (*s && p < q) { + int ch = *s++; + switch (ch) { + case '.': + if (s[-2] == '_') return -1; + if (s[0] == '_') return -1; + break; + case '_': + // disallow '__' + if (s[0] == '_') return -1; + continue; /* skip _ */ + default: + break; + } + *p++ = ch; + } + if (*s || p == q) return -1; /* reached end of string or buffer is full? */ + + /* last char cannot be '_' */ + if (s[-1] == '_') return -1; + + if (p != buf && p[-1] == '.') + return -1; /* no trailing zero */ + + /* cap with NUL */ + *p = 0; + + /* Run strtod on buf to get the value */ + char* endp; + errno = 0; + *ret = strtod(buf, &endp); + return (errno || *endp) ? -1 : 0; +} + +int toml_rtod(toml_raw_t src, double* ret_) +{ + char buf[100]; + return toml_rtod_ex(src, ret_, buf, sizeof(buf)); +} + + + + +int toml_rtos(toml_raw_t src, char** ret) +{ + int multiline = 0; + const char* sp; + const char* sq; + + *ret = 0; + if (!src) return -1; + + int qchar = src[0]; + int srclen = strlen(src); + if (! (qchar == '\'' || qchar == '"')) { + return -1; + } + + // triple quotes? + if (qchar == src[1] && qchar == src[2]) { + multiline = 1; + sp = src + 3; + sq = src + srclen - 3; + /* last 3 chars in src must be qchar */ + if (! (sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar)) + return -1; + + /* skip new line immediate after qchar */ + if (sp[0] == '\n') + sp++; + else if (sp[0] == '\r' && sp[1] == '\n') + sp += 2; + + } else { + sp = src + 1; + sq = src + srclen - 1; + /* last char in src must be qchar */ + if (! (sp <= sq && *sq == qchar)) + return -1; + } + + if (qchar == '\'') { + *ret = norm_lit_str(sp, sq - sp, + multiline, + 0, 0); + } else { + *ret = norm_basic_str(sp, sq - sp, + multiline, + 0, 0); + } + + return *ret ? 0 : -1; +} + + +toml_datum_t toml_string_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtos(toml_raw_at(arr, idx), &ret.u.s)); + return ret; +} + +toml_datum_t toml_bool_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_at(arr, idx), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_at(arr, idx), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_at(const toml_array_t* arr, int idx) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_at(arr, idx), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_at(arr, idx), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + } + } + return ret; +} + +toml_datum_t toml_string_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + toml_raw_t raw = toml_raw_in(arr, key); + if (raw) { + ret.ok = (0 == toml_rtos(raw, &ret.u.s)); + } + return ret; +} + +toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtob(toml_raw_in(arr, key), &ret.u.b)); + return ret; +} + +toml_datum_t toml_int_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtoi(toml_raw_in(arr, key), &ret.u.i)); + return ret; +} + +toml_datum_t toml_double_in(const toml_table_t* arr, const char* key) +{ + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtod(toml_raw_in(arr, key), &ret.u.d)); + return ret; +} + +toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key) +{ + toml_timestamp_t ts; + toml_datum_t ret; + memset(&ret, 0, sizeof(ret)); + ret.ok = (0 == toml_rtots(toml_raw_in(arr, key), &ts)); + if (ret.ok) { + ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts))); + if (ret.ok) { + *ret.u.ts = ts; + } + } + return ret; +} diff --git a/src/toml.h b/src/toml.h new file mode 100644 index 0000000..19f6f64 --- /dev/null +++ b/src/toml.h @@ -0,0 +1,175 @@ +/* + MIT License + + Copyright (c) 2017 - 2019 CK Tan + https://github.com/cktan/tomlc99 + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef TOML_H +#define TOML_H + + +#include +#include + + +#ifdef __cplusplus +#define TOML_EXTERN extern "C" +#else +#define TOML_EXTERN extern +#endif + +typedef struct toml_timestamp_t toml_timestamp_t; +typedef struct toml_table_t toml_table_t; +typedef struct toml_array_t toml_array_t; +typedef struct toml_datum_t toml_datum_t; + +/* Parse a file. Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, + char* errbuf, + int errbufsz); + +/* Parse a string containing the full config. + * Return a table on success, or 0 otherwise. + * Caller must toml_free(the-return-value) after use. + */ +TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */ + char* errbuf, + int errbufsz); + +/* Free the table returned by toml_parse() or toml_parse_file(). Once + * this function is called, any handles accessed through this tab + * directly or indirectly are no longer valid. + */ +TOML_EXTERN void toml_free(toml_table_t* tab); + + +/* Timestamp types. The year, month, day, hour, minute, second, z + * fields may be NULL if they are not relevant. e.g. In a DATE + * type, the hour, minute, second and z fields will be NULLs. + */ +struct toml_timestamp_t { + struct { /* internal. do not use. */ + int year, month, day; + int hour, minute, second, millisec; + char z[10]; + } __buffer; + int *year, *month, *day; + int *hour, *minute, *second, *millisec; + char* z; +}; + + +/*----------------------------------------------------------------- + * Enhanced access methods + */ +struct toml_datum_t { + int ok; + union { + toml_timestamp_t* ts; /* ts must be freed after use */ + char* s; /* string value. s must be freed after use */ + int b; /* bool value */ + int64_t i; /* int value */ + double d; /* double value */ + } u; +}; + +/* on arrays: */ +/* ... retrieve size of array. */ +TOML_EXTERN int toml_array_nelem(const toml_array_t* arr); +/* ... retrieve values using index. */ +TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx); +/* ... retrieve array or table using index. */ +TOML_EXTERN toml_array_t* toml_array_at(const toml_array_t* arr, int idx); +TOML_EXTERN toml_table_t* toml_table_at(const toml_array_t* arr, int idx); + +/* on tables: */ +/* ... retrieve the key in table at keyidx. Return 0 if out of range. */ +TOML_EXTERN const char* toml_key_in(const toml_table_t* tab, int keyidx); +/* ... retrieve values using key. */ +TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t* arr, const char* key); +TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key); +/* .. retrieve array or table using key. */ +TOML_EXTERN toml_array_t* toml_array_in(const toml_table_t* tab, + const char* key); +TOML_EXTERN toml_table_t* toml_table_in(const toml_table_t* tab, + const char* key); + +/*----------------------------------------------------------------- + * lesser used + */ +/* Return the array kind: 't'able, 'a'rray, 'v'alue */ +TOML_EXTERN char toml_array_kind(const toml_array_t* arr); + +/* For array kind 'v'alue, return the type of values + i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp + 0 if unknown +*/ +TOML_EXTERN char toml_array_type(const toml_array_t* arr); + +/* Return the key of an array */ +TOML_EXTERN const char* toml_array_key(const toml_array_t* arr); + +/* Return the number of key-values in a table */ +TOML_EXTERN int toml_table_nkval(const toml_table_t* tab); + +/* Return the number of arrays in a table */ +TOML_EXTERN int toml_table_narr(const toml_table_t* tab); + +/* Return the number of sub-tables in a table */ +TOML_EXTERN int toml_table_ntab(const toml_table_t* tab); + +/* Return the key of a table*/ +TOML_EXTERN const char* toml_table_key(const toml_table_t* tab); + +/*-------------------------------------------------------------- + * misc + */ +TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret); +TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]); +TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t), + void (*xxfree)(void*)); + + +/*-------------------------------------------------------------- + * deprecated + */ +/* A raw value, must be processed by toml_rto* before using. */ +typedef const char* toml_raw_t; +TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key); +TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t* arr, int idx); +TOML_EXTERN int toml_rtos(toml_raw_t s, char** ret); +TOML_EXTERN int toml_rtob(toml_raw_t s, int* ret); +TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t* ret); +TOML_EXTERN int toml_rtod(toml_raw_t s, double* ret); +TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double* ret, char* buf, int buflen); +TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t* ret); + + +#endif /* TOML_H */ diff --git a/src/tomlcpp.cpp b/src/tomlcpp.cpp new file mode 100644 index 0000000..b3c7f94 --- /dev/null +++ b/src/tomlcpp.cpp @@ -0,0 +1,334 @@ +/* + + MIT License + + Copyright (c) 2020 CK Tan + https://github.com/cktan/tomlcpp + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +*/ +#include +#include +#include +#include +#include "tomlcpp.hpp" +#include "toml.h" + +using namespace toml; +using std::string; +using std::vector; +using std::pair; + + +static void* toml_mymalloc(size_t sz) +{ + return new char[sz]; +} + +static void toml_myfree(void* p) +{ + if (p) { + char* pp = (char*) p; + delete[] pp; + } +} + + + +/** + * Keep track of memory to be freed when all references + * to the tree returned by toml::parse is no longer reachable. + */ +struct toml::Backing { + char* ptr = 0; + toml_table_t* root = 0; + Backing(const string& conf) { + ptr = new char[conf.length() + 1]; + strcpy(ptr, conf.c_str()); + } + ~Backing() { + if (ptr) delete[] ptr; + if (root) toml_free(root); + } +}; + + +pair Table::getString(const string& key) const +{ + string str; + toml_datum_t p = toml_string_in(m_table, key.c_str()); + if (p.ok) { + str = p.u.s; + toml_myfree(p.u.s); + } + return {p.ok, str}; +} + +pair Table::getBool(const string& key) const +{ + toml_datum_t p = toml_bool_in(m_table, key.c_str()); + return {p.ok, !!p.u.b}; + +} + +pair Table::getInt(const string& key) const +{ + toml_datum_t p = toml_int_in(m_table, key.c_str()); + return {p.ok, p.u.i}; +} + +pair Table::getDouble(const string& key) const +{ + toml_datum_t p = toml_double_in(m_table, key.c_str()); + return {p.ok, p.u.d}; +} + +pair Table::getTimestamp(const string& key) const +{ + Timestamp ret; + toml_datum_t p = toml_timestamp_in(m_table, key.c_str()); + if (p.ok) { + toml_timestamp_t& ts = *p.u.ts; + ret.year = (ts.year ? *ts.year : -1); + ret.month = (ts.month ? *ts.month : -1); + ret.day = (ts.day ? *ts.day : -1); + ret.hour = (ts.hour ? *ts.hour : -1); + ret.second = (ts.second ? *ts.second : -1); + ret.millisec = (ts.millisec ? *ts.millisec : -1); + ret.z = ts.z ? string(ts.z) : ""; + toml_myfree(p.u.ts); + } + return {p.ok, ret}; +} + + + +std::unique_ptr Table::getArray(const string& key) const +{ + toml_array_t* a = toml_array_in(m_table, key.c_str()); + if (!a) + return 0; + + auto ret = std::make_unique(a, m_backing); + return ret; +} + +std::unique_ptr Table::getTable(const string& key) const +{ + toml_table_t* t = toml_table_in(m_table, key.c_str()); + if (!t) + return 0; + + auto ret = std::make_unique
(t, m_backing); + return ret; +} + +vector Table::keys() const +{ + vector vec; + for (int i = 0; ; i++) { + const char* k = toml_key_in(m_table, i); + if (!k) break; + vec.push_back(k); + } + return vec; +} + + +char Array::kind() const +{ + return toml_array_kind(m_array); +} + +char Array::type() const +{ + return toml_array_type(m_array); +} + + +std::unique_ptr< vector> Array::getArrayVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector>(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_array_t* a = toml_array_at(m_array, i); + if (!a) + return 0; + + ret->push_back(Array(a, m_backing)); + } + + return ret; +} + +std::unique_ptr< vector
> Array::getTableVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector
>(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_table_t* t = toml_table_at(m_array, i); + if (!t) + return 0; + + ret->push_back(Table(t, m_backing)); + } + + return ret; +} + +std::unique_ptr< vector > Array::getStringVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector >(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_datum_t p = toml_string_at(m_array, i); + if (!p.ok) return 0; + ret->push_back(p.u.s); + toml_myfree(p.u.s); + } + + return ret; +} + + +std::unique_ptr< vector > Array::getBoolVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector >(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_datum_t p = toml_bool_at(m_array, i); + if (!p.ok) return 0; + ret->push_back(!!p.u.b); + } + + return ret; +} + + +std::unique_ptr< vector > Array::getIntVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector >(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_datum_t p = toml_int_at(m_array, i); + if (!p.ok) return 0; + ret->push_back(p.u.i); + } + + return ret; +} + + +std::unique_ptr< vector > Array::getTimestampVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector >(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_datum_t p = toml_timestamp_at(m_array, i); + if (!p.ok) return 0; + + toml_timestamp_t& ts = *p.u.ts; + Timestamp v; + v.year = (ts.year ? *ts.year : -1); + v.month = (ts.month ? *ts.month : -1); + v.day = (ts.day ? *ts.day : -1); + v.hour = (ts.hour ? *ts.hour : -1); + v.second = (ts.second ? *ts.second : -1); + v.millisec = (ts.millisec ? *ts.millisec : -1); + v.z = ts.z ? string(ts.z) : ""; + toml_myfree(p.u.ts); + + ret->push_back(v); + } + + return ret; +} + + +std::unique_ptr< vector > Array::getDoubleVector() const +{ + int top = toml_array_nelem(m_array); + if (top < 0) return 0; + + auto ret = std::make_unique< vector >(); + ret->reserve(top); + for (int i = 0; i < top; i++) { + toml_datum_t p = toml_double_at(m_array, i); + if (!p.ok) return 0; + ret->push_back(p.u.d); + } + + return ret; +} + + +int toml::Array::size() const +{ + return toml_array_nelem(m_array); +} + + +toml::Result toml::parse(const string& conf) +{ + toml::Result ret; + char errbuf[200]; + auto backing = std::make_shared(conf); + + toml_set_memutil(toml_mymalloc, toml_myfree); + toml_table_t* t = toml_parse(backing->ptr, errbuf, sizeof(errbuf)); + if (t) { + ret.table = std::make_shared
(t, backing); + backing->root = t; + } else { + ret.errmsg = (*errbuf) ? string(errbuf) : "unknown error"; + } + return ret; +} + + +toml::Result toml::parseFile(const string& path) +{ + toml::Result ret; + std::ifstream stream(path); + if (!stream) { + ret.errmsg = strerror(errno); + return ret; + } + string conf(std::istreambuf_iterator{stream}, {}); + return toml::parse(conf); +} diff --git a/src/tomlcpp.hpp b/src/tomlcpp.hpp new file mode 100644 index 0000000..7d8342b --- /dev/null +++ b/src/tomlcpp.hpp @@ -0,0 +1,128 @@ +/* + MIT License + + Copyright (c) 2020 CK Tan + https://github.com/cktan/tomlcpp + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ +#ifndef TOML_HPP +#define TOML_HPP + +#include + +struct toml_table_t; +struct toml_array_t; + +namespace toml { + + struct Backing; + class Array; + class Table; + using std::pair; + using std::string; + using std::vector; + + /* A Timestamp value */ + struct Timestamp { + // -1 means it is not valid + int year = -1; + int month = -1; + int day = -1; + int hour = -1; + int minute = -1; + int second = -1; + int millisec = -1; + string z; // "" if no timezone + }; + + /* A table in toml. You can extract value/table/array using a key. */ + class Table { + public: + vector keys() const; + + // get content + pair getString(const string& key) const; + pair getBool(const string& key) const; + pair getInt(const string& key) const; + pair getDouble(const string& key) const; + pair getTimestamp(const string& key) const; + std::unique_ptr
getTable(const string& key) const; + std::unique_ptr getArray(const string& key) const; + + // internal + Table(toml_table_t* t, std::shared_ptr backing) : m_table(t), m_backing(backing) {} + + private: + toml_table_t* const m_table = 0; + std::shared_ptr m_backing; + + Table() = delete; + }; + + + /* An array in toml. You can extract value/table/array using an index. */ + class Array { + public: + + // Content kind + // t:table, a:array, v:value + char kind() const; + + // For Value kind only, check the type of the value + // i:int, d:double, b:bool, s:string, t:time, D: date, T:timestamp, 0:unknown + char type() const; + + // Return the #elements in the array + int size() const; + + // For values, some conveniet methods to obtain vector of values + std::unique_ptr< vector > getStringVector() const; + std::unique_ptr< vector > getBoolVector() const; + std::unique_ptr< vector > getIntVector() const; + std::unique_ptr< vector > getDoubleVector() const; + std::unique_ptr< vector > getTimestampVector() const; + + // Obtain vectors of table or array + std::unique_ptr< vector
> getTableVector() const; + std::unique_ptr< vector> getArrayVector() const; + + // internal + Array(toml_array_t* a, std::shared_ptr backing) : m_array(a), m_backing(backing) {} + + private: + toml_array_t* const m_array = 0; + std::shared_ptr m_backing; + + Array() = delete; + }; + + + /* The main function: Parse */ + struct Result { + std::shared_ptr
table; + string errmsg; + }; + + Result parse(const string& conf); + Result parseFile(const string& path); +}; + + +#endif /* TOML_HPP */