diff options
Diffstat (limited to 'ext/offbase/offbase.hpp')
-rw-r--r-- | ext/offbase/offbase.hpp | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/ext/offbase/offbase.hpp b/ext/offbase/offbase.hpp new file mode 100644 index 00000000..acce8c4a --- /dev/null +++ b/ext/offbase/offbase.hpp @@ -0,0 +1,393 @@ +/* + * Offbase: a super-minimal in-filesystem JSON object persistence store + */ + +#ifndef OFFBASE_HPP__ +#define OFFBASE_HPP__ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <dirent.h> +#include <unistd.h> +#include <sys/stat.h> + +#include <vector> +#include <string> +#include <map> + +#include "json/json.hpp" + +#define OFFBASE_PATH_SEP "/" + +/** + * A super-minimal in-filesystem JSON object persistence store + */ +class offbase : public nlohmann::json +{ +public: + offbase(const char *p) : + nlohmann::json(nlohmann::json::object()), + _path(p), + _saved(nlohmann::json::object()) + { + this->load(); + } + + ~offbase() + { + this->commit(); + } + + /** + * Load this instance from disk, clearing any existing contents first + * + * If the 'errors' vector is NULL, false is returned and reading aborts + * on any error. If this parameter is non-NULL the paths of errors will + * be added to the vector and reading will continue. False will only be + * returned on really big errors like no path being defined. + * + * @param errors If specified, fill this vector with the paths to any objects that fail read + * @return True on success, false on fatal error + */ + inline bool load(std::vector<std::string> *errors = (std::vector<std::string> *)0) + { + if (!_path.length()) + return false; + *this = nlohmann::json::object(); + if (!_loadObj(_path,*this,errors)) + return false; + _saved = *(reinterpret_cast<nlohmann::json *>(this)); + return true; + } + + /** + * Commit any pending changes to this object to disk + * + * @return True on success or false if an I/O error occurred + */ + inline bool commit(std::vector<std::string> *errors = (std::vector<std::string> *)0) + { + if (!_path.length()) + return false; + if (!_commitObj(_path,*this,&_saved,errors)) + return false; + _saved = *(reinterpret_cast<nlohmann::json *>(this)); + return true; + } + + static inline std::string escapeKey(const std::string &k) + { + std::string e; + const char *ptr = k.data(); + const char *eof = ptr + k.length(); + char tmp[8]; + while (ptr != eof) { + if ( ((*ptr >= 'a')&&(*ptr <= 'z')) || ((*ptr >= 'A')&&(*ptr <= 'Z')) || ((*ptr >= '0')&&(*ptr <= '9')) || (*ptr == '.') || (*ptr == '_') || (*ptr == '-') || (*ptr == ',') ) + e.push_back(*ptr); + else { + snprintf(tmp,sizeof(tmp),"~%.2x",(unsigned int)*ptr); + e.append(tmp); + } + ++ptr; + } + return e; + } + + static inline std::string unescapeKey(const std::string &k) + { + std::string u; + const char *ptr = k.data(); + const char *eof = ptr + k.length(); + char tmp[8]; + while (ptr != eof) { + if (*ptr == '~') { + if (++ptr == eof) break; + tmp[0] = *ptr; + if (++ptr == eof) break; + tmp[1] = *(ptr++); + tmp[2] = (char)0; + u.push_back((char)strtol(tmp,(char **)0,16)); + } else { + u.push_back(*(ptr++)); + } + } + return u; + } + +private: + static inline bool _readFile(const char *path,std::string &buf) + { + char tmp[4096]; + FILE *f = fopen(path,"rb"); + if (f) { + for(;;) { + long n = (long)fread(tmp,1,sizeof(tmp),f); + if (n > 0) + buf.append(tmp,n); + else break; + } + fclose(f); + return true; + } + return false; + } + + static inline bool _loadArr(const std::string &path,nlohmann::json &arr,std::vector<std::string> *errors) + { + std::map<unsigned long,nlohmann::json> atmp; // place into an ordered container first because filesystem does not guarantee this + + struct dirent dbuf; + struct dirent *de; + DIR *d = opendir(path.c_str()); + if (d) { + while (!readdir_d(d,&dbuf,&de)) { + if (!de) break; + const std::string name(de->d_name); + if (name.length() != 12) continue; // array entries are XXXXXXXXXX.T + if (name[name.length()-2] == '.') { + if (name[name.length()-1] == 'V') { + std::string buf; + if (_readFile((path + OFFBASE_PATH_SEP + name).c_str(),buf)) { + try { + atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::parse(buf); + } catch ( ... ) { + if (errors) { + errors->push_back(path + OFFBASE_PATH_SEP + name); + } else { + return false; + } + } + } else if (errors) { + errors->push_back(path + OFFBASE_PATH_SEP + name); + } else return false; + } else if (name[name.length()-1] == 'O') { + if (!_loadObj(path + OFFBASE_PATH_SEP + name,atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::object(),errors)) + return false; + } else if (name[name.length()-1] == 'A') { + if (!_loadArr(path + OFFBASE_PATH_SEP + name,atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::array(),errors)) + return false; + } + } + } + closedir(d); + } else if (errors) { + errors->push_back(path); + } else return false; + + if (atmp.size() > 0) { + unsigned long lasti = 0; + for(std::map<unsigned long,nlohmann::json>::iterator i(atmp.begin());i!=atmp.end();++i) { + for(unsigned long k=lasti;k<i->first;++k) // fill any gaps with nulls + arr.push_back(nlohmann::json(std::nullptr_t)); + lasti = i->first; + arr.push_back(i->second); + } + } + + return true; + } + + static inline bool _loadObj(const std::string &path,nlohmann::json &obj,std::vector<std::string> *errors) + { + struct dirent dbuf; + struct dirent *de; + DIR *d = opendir(path.c_str()); + if (d) { + while (!readdir_d(d,&dbuf,&de)) { + if (!de) break; + if ((strcmp(de->d_name,".") == 0)||(strcmp(de->d_name,"..") == 0)) continue; // sanity check + const std::string name(de->d_name); + if (name.length() <= 2) continue; + if (name[name.length()-2] == '.') { + if (name[name.length()-1] == 'V') { + std::string buf; + if (_readFile((path + OFFBASE_PATH_SEP + name).c_str(),buf)) { + try { + obj[unescapeKey(name)] = nlohmann::json::parse(buf); + } catch ( ... ) { + if (errors) { + errors->push_back(path + OFFBASE_PATH_SEP + name); + } else { + return false; + } + } + } else if (errors) { + errors->push_back(path + OFFBASE_PATH_SEP + name); + } else return false; + } else if (name[name.length()-1] == 'O') { + if (!_loadObj(path + OFFBASE_PATH_SEP + name,obj[unescapeKey(name)] = nlohmann::json::object(),errors)) + return false; + } else if (name[name.length()-1] == 'A') { + if (!_loadArr(path + OFFBASE_PATH_SEP + name,obj[unescapeKey(name)] = nlohmann::json::array(),errors)) + return false; + } + } + } + closedir(d); + } else if (errors) { + errors->push_back(path); + } else return false; + return true; + } + + static inline void _rmDashRf(const std::string &path) + { + struct dirent dbuf; + struct dirent *de; + DIR *d = opendir(path.c_str()); + if (d) { + while (!readdir_r(d,&dbuf,&de)) { + if (!de) break; + if ((strcmp(de->d_name,".") == 0)||(strcmp(de->d_name,"..") == 0)) continue; // sanity check + const std::string full(path + OFFBASE_PATH_SEP + de->d_name); + if (unlink(full.c_str())) { + _rmDashRf(full); + rmdir(full.c_str()); + } + } + closedir(d); + } + rmdir(path.c_str()); + } + + static inline bool _commitArr(const std::string &path,const nlohmann::json &arr,const nlohmann::json *previous,std::vector<std::string> *errors) + { + char tmp[32]; + + if (!arr.is_array()) + return false; + + mkdir(path.c_str(),0755); + + for(unsigned long i=0;i<(unsigned long)arr.size();++i) { + const nlohmann::json &value = arr[i]; + + const nlohmann::json *next = (const nlohmann::json *)0; + if ((previous)&&(previous->is_array())&&(i < previous->size())) { + next = &((*previous)[i]); + if (*next == value) + continue; + } + + if (value.is_object()) { + snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i); + if (!_commitObj(path + tmp,value,next,errors)) + return false; + snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i); + unlink((path + tmp).c_str()); + snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + } else if (value.is_array()) { + snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i); + if (!_commitArr(path + tmp,value,next,errors)) + return false; + snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i); + unlink((path + tmp).c_str()); + } else { + snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i); + FILE *f = fopen((path + tmp).c_str(),"w"); + if (f) { + const std::string v(value.dump()); + if (fwrite(v.c_str(),v.length(),1,f) != 1) { + fclose(f); + return false; + } else { + fclose(f); + } + } else { + return false; + } + snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + } + } + + if ((previous)&&(previous->is_array())) { + for(unsigned long i=(unsigned long)arr.size();i<(unsigned long)previous->size();++i) { + snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i); + unlink((path + tmp).c_str()); + snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i); + _rmDashRf(path + tmp); + } + } + + return true; + } + + static inline bool _commitObj(const std::string &path,const nlohmann::json &obj,const nlohmann::json *previous,std::vector<std::string> *errors) + { + if (!obj.is_object()) + return false; + + mkdir(path.c_str(),0755); + + for(nlohmann::json::const_iterator i(obj.begin());i!=obj.end();++i) { + if (i.key().length() == 0) + continue; + + const nlohmann::json *next = (const nlohmann::json *)0; + if ((previous)&&(previous->is_object())) { + nlohmann::json::const_iterator saved(previous->find(i.key())); + if (saved != previous->end()) { + next = &(saved.value()); + if (i.value() == *next) + continue; + } + } + + const std::string keyp(path + OFFBASE_PATH_SEP + escapeKey(i.key())); + if (i.value().is_object()) { + if (!_commitObj(keyp + ".O",i.value(),next,errors)) + return false; + unlink((keyp + ".V").c_str()); + _rmDashRf(keyp + ".A"); + } else if (i.value().is_array()) { + if (!_commitArr(keyp + ".A",i.value(),next,errors)) + return false; + unlink((keyp + ".V").c_str()); + _rmDashRf(keyp + ".O"); + } else { + FILE *f = fopen((keyp + ".V").c_str(),"w"); + if (f) { + const std::string v(i.value().dump()); + if (fwrite(v.c_str(),v.length(),1,f) != 1) { + fclose(f); + return false; + } else { + fclose(f); + } + } else { + return false; + } + _rmDashRf(keyp + ".A"); + _rmDashRf(keyp + ".O"); + } + } + + if ((previous)&&(previous->is_object())) { + for(nlohmann::json::const_iterator i(previous->begin());i!=previous->end();++i) { + if ((i.key().length() > 0)&&(obj.find(i.key()) == obj.end())) { + const std::string keyp(path + OFFBASE_PATH_SEP + escapeKey(i.key())); + unlink((keyp + ".V").c_str()); + _rmDashRf(keyp + ".A"); + _rmDashRf(keyp + ".O"); + } + } + } + + return true; + } + + std::string _path; + nlohmann::json _saved; +}; + +#endif |