offbase.hpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. /*
  2. * Offbase: a super-minimal in-filesystem JSON object persistence store
  3. */
  4. #ifndef OFFBASE_HPP__
  5. #define OFFBASE_HPP__
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include <string.h>
  9. #include <dirent.h>
  10. #include <unistd.h>
  11. #include <sys/stat.h>
  12. #include <vector>
  13. #include <string>
  14. #include <map>
  15. #include "json/json.hpp"
  16. #define OFFBASE_PATH_SEP "/"
  17. /**
  18. * A super-minimal in-filesystem JSON object persistence store
  19. */
  20. class offbase : public nlohmann::json
  21. {
  22. public:
  23. offbase(const char *p) :
  24. nlohmann::json(nlohmann::json::object()),
  25. _path(p),
  26. _saved(nlohmann::json::object())
  27. {
  28. this->load();
  29. }
  30. ~offbase()
  31. {
  32. this->commit();
  33. }
  34. /**
  35. * Load this instance from disk, clearing any existing contents first
  36. *
  37. * If the 'errors' vector is NULL, false is returned and reading aborts
  38. * on any error. If this parameter is non-NULL the paths of errors will
  39. * be added to the vector and reading will continue. False will only be
  40. * returned on really big errors like no path being defined.
  41. *
  42. * @param errors If specified, fill this vector with the paths to any objects that fail read
  43. * @return True on success, false on fatal error
  44. */
  45. inline bool load(std::vector<std::string> *errors = (std::vector<std::string> *)0)
  46. {
  47. if (!_path.length())
  48. return false;
  49. *this = nlohmann::json::object();
  50. if (!_loadObj(_path,*this,errors))
  51. return false;
  52. _saved = *(reinterpret_cast<nlohmann::json *>(this));
  53. return true;
  54. }
  55. /**
  56. * Commit any pending changes to this object to disk
  57. *
  58. * @return True on success or false if an I/O error occurred
  59. */
  60. inline bool commit(std::vector<std::string> *errors = (std::vector<std::string> *)0)
  61. {
  62. if (!_path.length())
  63. return false;
  64. if (!_commitObj(_path,*this,&_saved,errors))
  65. return false;
  66. _saved = *(reinterpret_cast<nlohmann::json *>(this));
  67. return true;
  68. }
  69. static inline std::string escapeKey(const std::string &k)
  70. {
  71. std::string e;
  72. const char *ptr = k.data();
  73. const char *eof = ptr + k.length();
  74. char tmp[8];
  75. while (ptr != eof) {
  76. if ( ((*ptr >= 'a')&&(*ptr <= 'z')) || ((*ptr >= 'A')&&(*ptr <= 'Z')) || ((*ptr >= '0')&&(*ptr <= '9')) || (*ptr == '.') || (*ptr == '_') || (*ptr == '-') || (*ptr == ',') )
  77. e.push_back(*ptr);
  78. else {
  79. snprintf(tmp,sizeof(tmp),"~%.2x",(unsigned int)*ptr);
  80. e.append(tmp);
  81. }
  82. ++ptr;
  83. }
  84. return e;
  85. }
  86. static inline std::string unescapeKey(const std::string &k)
  87. {
  88. std::string u;
  89. const char *ptr = k.data();
  90. const char *eof = ptr + k.length();
  91. char tmp[8];
  92. while (ptr != eof) {
  93. if (*ptr == '~') {
  94. if (++ptr == eof) break;
  95. tmp[0] = *ptr;
  96. if (++ptr == eof) break;
  97. tmp[1] = *(ptr++);
  98. tmp[2] = (char)0;
  99. u.push_back((char)strtol(tmp,(char **)0,16));
  100. } else {
  101. u.push_back(*(ptr++));
  102. }
  103. }
  104. return u;
  105. }
  106. private:
  107. static inline bool _readFile(const char *path,std::string &buf)
  108. {
  109. char tmp[4096];
  110. FILE *f = fopen(path,"rb");
  111. if (f) {
  112. for(;;) {
  113. long n = (long)fread(tmp,1,sizeof(tmp),f);
  114. if (n > 0)
  115. buf.append(tmp,n);
  116. else break;
  117. }
  118. fclose(f);
  119. return true;
  120. }
  121. return false;
  122. }
  123. static inline bool _loadArr(const std::string &path,nlohmann::json &arr,std::vector<std::string> *errors)
  124. {
  125. std::map<unsigned long,nlohmann::json> atmp; // place into an ordered container first because filesystem does not guarantee this
  126. struct dirent dbuf;
  127. struct dirent *de;
  128. DIR *d = opendir(path.c_str());
  129. if (d) {
  130. while (!readdir_d(d,&dbuf,&de)) {
  131. if (!de) break;
  132. const std::string name(de->d_name);
  133. if (name.length() != 12) continue; // array entries are XXXXXXXXXX.T
  134. if (name[name.length()-2] == '.') {
  135. if (name[name.length()-1] == 'V') {
  136. std::string buf;
  137. if (_readFile((path + OFFBASE_PATH_SEP + name).c_str(),buf)) {
  138. try {
  139. atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::parse(buf);
  140. } catch ( ... ) {
  141. if (errors) {
  142. errors->push_back(path + OFFBASE_PATH_SEP + name);
  143. } else {
  144. return false;
  145. }
  146. }
  147. } else if (errors) {
  148. errors->push_back(path + OFFBASE_PATH_SEP + name);
  149. } else return false;
  150. } else if (name[name.length()-1] == 'O') {
  151. if (!_loadObj(path + OFFBASE_PATH_SEP + name,atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::object(),errors))
  152. return false;
  153. } else if (name[name.length()-1] == 'A') {
  154. if (!_loadArr(path + OFFBASE_PATH_SEP + name,atmp[strtoul(name.substr(0,10).c_str(),(char **)0,16)] = nlohmann::json::array(),errors))
  155. return false;
  156. }
  157. }
  158. }
  159. closedir(d);
  160. } else if (errors) {
  161. errors->push_back(path);
  162. } else return false;
  163. if (atmp.size() > 0) {
  164. unsigned long lasti = 0;
  165. for(std::map<unsigned long,nlohmann::json>::iterator i(atmp.begin());i!=atmp.end();++i) {
  166. for(unsigned long k=lasti;k<i->first;++k) // fill any gaps with nulls
  167. arr.push_back(nlohmann::json(std::nullptr_t));
  168. lasti = i->first;
  169. arr.push_back(i->second);
  170. }
  171. }
  172. return true;
  173. }
  174. static inline bool _loadObj(const std::string &path,nlohmann::json &obj,std::vector<std::string> *errors)
  175. {
  176. struct dirent dbuf;
  177. struct dirent *de;
  178. DIR *d = opendir(path.c_str());
  179. if (d) {
  180. while (!readdir_d(d,&dbuf,&de)) {
  181. if (!de) break;
  182. if ((strcmp(de->d_name,".") == 0)||(strcmp(de->d_name,"..") == 0)) continue; // sanity check
  183. const std::string name(de->d_name);
  184. if (name.length() <= 2) continue;
  185. if (name[name.length()-2] == '.') {
  186. if (name[name.length()-1] == 'V') {
  187. std::string buf;
  188. if (_readFile((path + OFFBASE_PATH_SEP + name).c_str(),buf)) {
  189. try {
  190. obj[unescapeKey(name)] = nlohmann::json::parse(buf);
  191. } catch ( ... ) {
  192. if (errors) {
  193. errors->push_back(path + OFFBASE_PATH_SEP + name);
  194. } else {
  195. return false;
  196. }
  197. }
  198. } else if (errors) {
  199. errors->push_back(path + OFFBASE_PATH_SEP + name);
  200. } else return false;
  201. } else if (name[name.length()-1] == 'O') {
  202. if (!_loadObj(path + OFFBASE_PATH_SEP + name,obj[unescapeKey(name)] = nlohmann::json::object(),errors))
  203. return false;
  204. } else if (name[name.length()-1] == 'A') {
  205. if (!_loadArr(path + OFFBASE_PATH_SEP + name,obj[unescapeKey(name)] = nlohmann::json::array(),errors))
  206. return false;
  207. }
  208. }
  209. }
  210. closedir(d);
  211. } else if (errors) {
  212. errors->push_back(path);
  213. } else return false;
  214. return true;
  215. }
  216. static inline void _rmDashRf(const std::string &path)
  217. {
  218. struct dirent dbuf;
  219. struct dirent *de;
  220. DIR *d = opendir(path.c_str());
  221. if (d) {
  222. while (!readdir_r(d,&dbuf,&de)) {
  223. if (!de) break;
  224. if ((strcmp(de->d_name,".") == 0)||(strcmp(de->d_name,"..") == 0)) continue; // sanity check
  225. const std::string full(path + OFFBASE_PATH_SEP + de->d_name);
  226. if (unlink(full.c_str())) {
  227. _rmDashRf(full);
  228. rmdir(full.c_str());
  229. }
  230. }
  231. closedir(d);
  232. }
  233. rmdir(path.c_str());
  234. }
  235. static inline bool _commitArr(const std::string &path,const nlohmann::json &arr,const nlohmann::json *previous,std::vector<std::string> *errors)
  236. {
  237. char tmp[32];
  238. if (!arr.is_array())
  239. return false;
  240. mkdir(path.c_str(),0755);
  241. for(unsigned long i=0;i<(unsigned long)arr.size();++i) {
  242. const nlohmann::json &value = arr[i];
  243. const nlohmann::json *next = (const nlohmann::json *)0;
  244. if ((previous)&&(previous->is_array())&&(i < previous->size())) {
  245. next = &((*previous)[i]);
  246. if (*next == value)
  247. continue;
  248. }
  249. if (value.is_object()) {
  250. snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i);
  251. if (!_commitObj(path + tmp,value,next,errors))
  252. return false;
  253. snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i);
  254. unlink((path + tmp).c_str());
  255. snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i);
  256. _rmDashRf(path + tmp);
  257. } else if (value.is_array()) {
  258. snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i);
  259. if (!_commitArr(path + tmp,value,next,errors))
  260. return false;
  261. snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i);
  262. _rmDashRf(path + tmp);
  263. snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i);
  264. unlink((path + tmp).c_str());
  265. } else {
  266. snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i);
  267. FILE *f = fopen((path + tmp).c_str(),"w");
  268. if (f) {
  269. const std::string v(value.dump());
  270. if (fwrite(v.c_str(),v.length(),1,f) != 1) {
  271. fclose(f);
  272. return false;
  273. } else {
  274. fclose(f);
  275. }
  276. } else {
  277. return false;
  278. }
  279. snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i);
  280. _rmDashRf(path + tmp);
  281. snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i);
  282. _rmDashRf(path + tmp);
  283. }
  284. }
  285. if ((previous)&&(previous->is_array())) {
  286. for(unsigned long i=(unsigned long)arr.size();i<(unsigned long)previous->size();++i) {
  287. snprintf(tmp,sizeof(tmp),"%s%.10lx.V",OFFBASE_PATH_SEP,i);
  288. unlink((path + tmp).c_str());
  289. snprintf(tmp,sizeof(tmp),"%s%.10lx.A",OFFBASE_PATH_SEP,i);
  290. _rmDashRf(path + tmp);
  291. snprintf(tmp,sizeof(tmp),"%s%.10lx.O",OFFBASE_PATH_SEP,i);
  292. _rmDashRf(path + tmp);
  293. }
  294. }
  295. return true;
  296. }
  297. static inline bool _commitObj(const std::string &path,const nlohmann::json &obj,const nlohmann::json *previous,std::vector<std::string> *errors)
  298. {
  299. if (!obj.is_object())
  300. return false;
  301. mkdir(path.c_str(),0755);
  302. for(nlohmann::json::const_iterator i(obj.begin());i!=obj.end();++i) {
  303. if (i.key().length() == 0)
  304. continue;
  305. const nlohmann::json *next = (const nlohmann::json *)0;
  306. if ((previous)&&(previous->is_object())) {
  307. nlohmann::json::const_iterator saved(previous->find(i.key()));
  308. if (saved != previous->end()) {
  309. next = &(saved.value());
  310. if (i.value() == *next)
  311. continue;
  312. }
  313. }
  314. const std::string keyp(path + OFFBASE_PATH_SEP + escapeKey(i.key()));
  315. if (i.value().is_object()) {
  316. if (!_commitObj(keyp + ".O",i.value(),next,errors))
  317. return false;
  318. unlink((keyp + ".V").c_str());
  319. _rmDashRf(keyp + ".A");
  320. } else if (i.value().is_array()) {
  321. if (!_commitArr(keyp + ".A",i.value(),next,errors))
  322. return false;
  323. unlink((keyp + ".V").c_str());
  324. _rmDashRf(keyp + ".O");
  325. } else {
  326. FILE *f = fopen((keyp + ".V").c_str(),"w");
  327. if (f) {
  328. const std::string v(i.value().dump());
  329. if (fwrite(v.c_str(),v.length(),1,f) != 1) {
  330. fclose(f);
  331. return false;
  332. } else {
  333. fclose(f);
  334. }
  335. } else {
  336. return false;
  337. }
  338. _rmDashRf(keyp + ".A");
  339. _rmDashRf(keyp + ".O");
  340. }
  341. }
  342. if ((previous)&&(previous->is_object())) {
  343. for(nlohmann::json::const_iterator i(previous->begin());i!=previous->end();++i) {
  344. if ((i.key().length() > 0)&&(obj.find(i.key()) == obj.end())) {
  345. const std::string keyp(path + OFFBASE_PATH_SEP + escapeKey(i.key()));
  346. unlink((keyp + ".V").c_str());
  347. _rmDashRf(keyp + ".A");
  348. _rmDashRf(keyp + ".O");
  349. }
  350. }
  351. }
  352. return true;
  353. }
  354. std::string _path;
  355. nlohmann::json _saved;
  356. };
  357. #endif