Fork of the espurna firmware for `mhsw` switches
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
8.2 KiB

Terminal: change command-line parser (#2247) Change the underlying command line handling: - switch to a custom parser, inspired by redis / sds - update terminalRegisterCommand signature, pass only bare minimum - clean-up `help` & `commands`. update settings `set`, `get` and `del` - allow our custom test suite to run command-line tests - clean-up Stream IO to allow us to print large things into debug stream (for example, `eeprom.dump`) - send parsing errors to the debug log As a proof of concept, introduce `TERMINAL_MQTT_SUPPORT` and `TERMINAL_WEB_API_SUPPORT` - MQTT subscribes to the `<root>/cmd/set` and sends response to the `<root>/cmd`. We can't output too much, as we don't have any large-send API. - Web API listens to the `/api/cmd?apikey=...&line=...` (or PUT, params inside the body). This one is intended as a possible replacement of the `API_SUPPORT`. Internals introduce a 'task' around the AsyncWebServerRequest object that will simulate what WiFiClient does and push data into it continuously, switching between CONT and SYS. Both are experimental. We only accept a single command and not every command is updated to use Print `ctx.output` object. We are also somewhat limited by the Print / Stream overall, perhaps I am overestimating the usefulness of Arduino compatibility to such an extent :) Web API handler can also sometimes show only part of the result, whenever the command tries to yield() by itself waiting for something. Perhaps we would need to create a custom request handler for that specific use-case.
4 years ago
  1. /*
  2. Part of the TERMINAL MODULE
  3. Copyright (C) 2016-2019 by Xose Pérez <xose dot perez at gmail dot com>
  4. Copyright (C) 2020 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
  5. */
  6. #include <vector>
  7. #include <cctype>
  8. #include "terminal_parsing.h"
  9. namespace terminal {
  10. namespace parsing {
  11. // c/p with minor modifications from redis / sds, so that we don't have to roll a custom parser
  12. // ref:
  13. // - https://github.com/antirez/sds/blob/master/sds.c
  14. // - https://github.com/antirez/redis/blob/unstable/src/networking.c
  15. //
  16. // Things are kept mostly the same, we are replacing Redis-specific things:
  17. // - sds structure -> String
  18. // - sds array -> std::vector<String>
  19. // - we return always return custom structure, nullptr can no longer be used
  20. // to notify about the missing / unterminated / mismatching quotes
  21. // - hex_... function helpers types are changed
  22. // Original code is part of the SDSLib 2.0 -- A C dynamic strings library
  23. // *
  24. // * Copyright (c) 2006-2015, Salvatore Sanfilippo <antirez at gmail dot com>
  25. // * Copyright (c) 2015, Oran Agra
  26. // * Copyright (c) 2015, Redis Labs, Inc
  27. // * All rights reserved.
  28. // *
  29. // * Redistribution and use in source and binary forms, with or without
  30. // * modification, are permitted provided that the following conditions are met:
  31. // *
  32. // * * Redistributions of source code must retain the above copyright notice,
  33. // * this list of conditions and the following disclaimer.
  34. // * * Redistributions in binary form must reproduce the above copyright
  35. // * notice, this list of conditions and the following disclaimer in the
  36. // * documentation and/or other materials provided with the distribution.
  37. // * * Neither the name of Redis nor the names of its contributors may be used
  38. // * to endorse or promote products derived from this software without
  39. // * specific prior written permission.
  40. // *
  41. // * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  42. // * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  43. // * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  44. // * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  45. // * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  46. // * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  47. // * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  48. // * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  49. // * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  50. // * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  51. // * POSSIBILITY OF SUCH DAMAGE.
  52. // Helper functions to handle \xHH codes
  53. static bool is_hex_digit(char c) {
  54. return (c >= '0' && c <= '9') \
  55. ||(c >= 'a' && c <= 'f') \
  56. ||(c >= 'A' && c <= 'F');
  57. }
  58. static char hex_digit_to_int(char c) {
  59. switch (c) {
  60. case '0': return 0;
  61. case '1': return 1;
  62. case '2': return 2;
  63. case '3': return 3;
  64. case '4': return 4;
  65. case '5': return 5;
  66. case '6': return 6;
  67. case '7': return 7;
  68. case '8': return 8;
  69. case '9': return 9;
  70. case 'a': case 'A': return 10;
  71. case 'b': case 'B': return 11;
  72. case 'c': case 'C': return 12;
  73. case 'd': case 'D': return 13;
  74. case 'e': case 'E': return 14;
  75. case 'f': case 'F': return 15;
  76. default: return 0;
  77. }
  78. }
  79. // Our port of `sdssplitargs`
  80. CommandLine parse_commandline(const char *line) {
  81. const char *p = line;
  82. CommandLine result {{}, 0};
  83. result.argv.reserve(4);
  84. String current;
  85. while(1) {
  86. /* skip blanks */
  87. while(*p && isspace(*p)) p++;
  88. if (*p) {
  89. /* get a token */
  90. int inq=0; /* set to 1 if we are in "quotes" */
  91. int insq=0; /* set to 1 if we are in 'single quotes' */
  92. int done=0;
  93. while(!done) {
  94. if (inq) {
  95. if (*p == '\\' && *(p+1) == 'x' &&
  96. is_hex_digit(*(p+2)) &&
  97. is_hex_digit(*(p+3)))
  98. {
  99. // XXX: make sure that we append `char` or `char[]`,
  100. // even with -funsigned-char this can accidentally append itoa conversion
  101. unsigned char byte =
  102. (hex_digit_to_int(*(p+2))*16)+
  103. hex_digit_to_int(*(p+3));
  104. char buf[2] { static_cast<char>(byte), 0x00 };
  105. current += buf;
  106. p += 3;
  107. } else if (*p == '\\' && *(p+1)) {
  108. char c;
  109. p++;
  110. switch(*p) {
  111. case 'n': c = '\n'; break;
  112. case 'r': c = '\r'; break;
  113. case 't': c = '\t'; break;
  114. case 'b': c = '\b'; break;
  115. case 'a': c = '\a'; break;
  116. default: c = *p; break;
  117. }
  118. current += c;
  119. } else if (*p == '"') {
  120. /* closing quote must be followed by a space or
  121. * nothing at all. */
  122. if (*(p+1) && !isspace(*(p+1))) goto err;
  123. done=1;
  124. } else if (!*p) {
  125. /* unterminated quotes */
  126. goto err;
  127. } else {
  128. char buf[2] {*p, '\0'};
  129. current += buf;
  130. }
  131. } else if (insq) {
  132. if (*p == '\\' && *(p+1) == '\'') {
  133. p++;
  134. current += '\'';
  135. } else if (*p == '\'') {
  136. /* closing quote must be followed by a space or
  137. * nothing at all. */
  138. if (*(p+1) && !isspace(*(p+1))) goto err;
  139. done=1;
  140. } else if (!*p) {
  141. /* unterminated quotes */
  142. goto err;
  143. } else {
  144. char buf[2] {*p, '\0'};
  145. current += buf;
  146. }
  147. } else {
  148. switch(*p) {
  149. case ' ':
  150. case '\n':
  151. case '\r':
  152. case '\t':
  153. case '\0':
  154. done=1;
  155. break;
  156. case '"':
  157. inq=1;
  158. break;
  159. case '\'':
  160. insq=1;
  161. break;
  162. default: {
  163. char buf[2] {*p, '\0'};
  164. current += buf;
  165. break;
  166. }
  167. }
  168. }
  169. if (*p) p++;
  170. }
  171. /* add the token to the vector */
  172. result.argv.emplace_back(std::move(current));
  173. ++result.argc;
  174. } else {
  175. /* Even on empty input string return something not NULL. */
  176. return result;
  177. }
  178. }
  179. err:
  180. result.argc = 0;
  181. result.argv.clear();
  182. return result;
  183. }
  184. // Fowler–Noll–Vo hash function to hash command strings that treats input as lowercase
  185. // ref: https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function
  186. template<>
  187. size_t LowercaseFnv1Hash<String>::operator()(const String& str) const {
  188. constexpr uint32_t fnv_prime = 16777619u;
  189. constexpr uint32_t fnv_basis = 2166136261u;
  190. uint32_t hash = fnv_basis;
  191. for (size_t idx = 0; idx < str.length(); ++idx) {
  192. // TODO: String::operator[] is slightly slower here
  193. // does not happen with the std::string
  194. hash = hash ^ static_cast<uint32_t>(tolower(str.c_str()[idx]));
  195. hash = hash * fnv_prime;
  196. }
  197. return hash;
  198. }
  199. template<>
  200. bool LowercaseEquals<String>::operator()(const String& lhs, const String& rhs) const {
  201. return lhs.equalsIgnoreCase(rhs);
  202. }
  203. } // namespace parsing
  204. } // namespace terminal