ConfigParser.cc
Go to the documentation of this file.
1 /*
2  * Copyright (C) 1996-2025 The Squid Software Foundation and contributors
3  *
4  * Squid software is distributed under GPLv2+ license and includes
5  * contributions from numerous individuals and organizations.
6  * Please see the COPYING and CONTRIBUTORS files for details.
7  */
8 
9 #include "squid.h"
10 #include "acl/Gadgets.h"
11 #include "base/Here.h"
12 #include "base/RegexPattern.h"
13 #include "cache_cf.h"
14 #include "ConfigParser.h"
15 #include "debug/Stream.h"
16 #include "fatal.h"
17 #include "globals.h"
18 #include "neighbors.h"
19 #include "sbuf/Stream.h"
20 
22 bool ConfigParser::StrictMode = true;
23 std::stack<ConfigParser::CfgFile *> ConfigParser::CfgFiles;
25 const char *ConfigParser::CfgLine = nullptr;
26 const char *ConfigParser::CfgPos = nullptr;
27 std::queue<char *> ConfigParser::CfgLineTokens_;
28 bool ConfigParser::AllowMacros_ = false;
30 bool ConfigParser::ParseKvPair_ = false;
33 bool ConfigParser::PreviewMode_ = false;
34 
35 static const char *SQUID_ERROR_TOKEN = "[invalid token]";
36 
37 void
39 {
40  shutting_down = 1;
41  if (!CfgFiles.empty()) {
42  std::ostringstream message;
43  CfgFile *f = CfgFiles.top();
44  message << "Bungled " << f->filePath << " line " << f->lineNo <<
45  ": " << f->currentLine << std::endl;
46  CfgFiles.pop();
47  delete f;
48  while (!CfgFiles.empty()) {
49  f = CfgFiles.top();
50  message << " included from " << f->filePath << " line " <<
51  f->lineNo << ": " << f->currentLine << std::endl;
52  CfgFiles.pop();
53  delete f;
54  }
55  message << " included from " << cfg_filename << " line " <<
56  config_lineno << ": " << config_input_line << std::endl;
57  std::string msg = message.str();
58  fatalf("%s", msg.c_str());
59  } else
60  fatalf("Bungled %s line %d: %s",
62 }
63 
64 char *
66 {
68  return ConfigParser::NextToken();
69 
70  static int fromFile = 0;
71  static FILE *wordFile = nullptr;
72 
73  char *t;
74  static char buf[CONFIG_LINE_LIMIT];
75 
76  do {
77 
78  if (!fromFile) {
79  ConfigParser::TokenType tokenType;
80  t = ConfigParser::NextElement(tokenType);
81  if (!t) {
82  return nullptr;
83  } else if (*t == '\"' || *t == '\'') {
84  /* quote found, start reading from file */
85  debugs(3, 8,"Quoted token found : " << t);
86  char *fn = ++t;
87 
88  while (*t && *t != '\"' && *t != '\'')
89  ++t;
90 
91  *t = '\0';
92 
93  if ((wordFile = fopen(fn, "r")) == nullptr) {
94  debugs(3, DBG_CRITICAL, "ERROR: Can not open file " << fn << " for reading");
95  return nullptr;
96  }
97 
98 #if _SQUID_WINDOWS_
99  setmode(fileno(wordFile), O_TEXT);
100 #endif
101 
102  fromFile = 1;
103  } else {
104  return t;
105  }
106  }
107 
108  /* fromFile */
109  if (fgets(buf, sizeof(buf), wordFile) == nullptr) {
110  /* stop reading from file */
111  fclose(wordFile);
112  wordFile = nullptr;
113  fromFile = 0;
114  return nullptr;
115  } else {
116  char *t2, *t3;
117  t = buf;
118  /* skip leading and trailing white space */
119  t += strspn(buf, w_space);
120  t2 = t + strcspn(t, w_space);
121  t3 = t2 + strspn(t2, w_space);
122 
123  while (*t3 && *t3 != '#') {
124  t2 = t3 + strcspn(t3, w_space);
125  t3 = t2 + strspn(t2, w_space);
126  }
127 
128  *t2 = '\0';
129  }
130 
131  /* skip comments */
132  /* skip blank lines */
133  } while ( *t == '#' || !*t );
134 
135  return t;
136 }
137 
138 char *
139 ConfigParser::UnQuote(const char *token, const char **next)
140 {
141  const char *errorStr = nullptr;
142  const char *errorPos = nullptr;
143  char quoteChar = *token;
144  assert(quoteChar == '"' || quoteChar == '\'');
145  LOCAL_ARRAY(char, UnQuoted, CONFIG_LINE_LIMIT);
146  const char *s = token + 1;
147  char *d = UnQuoted;
148  /* scan until the end of the quoted string, handling escape sequences*/
149  while (*s && *s != quoteChar && !errorStr && (size_t)(d - UnQuoted) < sizeof(UnQuoted) - 1) {
150  if (*s == '\\') {
151  if (s[1] == '\0') {
152  errorStr = "Unterminated escape sequence";
153  errorPos = s;
154  break;
155  }
156  s++;
157  switch (*s) {
158  case 'r':
159  *d = '\r';
160  break;
161  case 'n':
162  *d = '\n';
163  break;
164  case 't':
165  *d = '\t';
166  break;
167  default:
168  if (isalnum(*s)) {
169  errorStr = "Unsupported escape sequence";
170  errorPos = s;
171  }
172  *d = *s;
173  break;
174  }
175  } else
176  *d = *s;
177  ++s;
178  ++d;
179  }
180 
181  if (*s != quoteChar && !errorStr) {
182  errorStr = "missing quote char at the end of quoted string";
183  errorPos = s - 1;
184  }
185  // The end of token
186  *d = '\0';
187 
188  // We are expecting a separator after quoted string, space or one of "()#"
189  if (!errorStr && *(s + 1) != '\0' && !strchr(w_space "()#", *(s + 1))) {
190  errorStr = "Expecting space after the end of quoted token";
191  errorPos = token;
192  }
193 
194  if (errorStr) {
195  if (PreviewMode_)
196  xstrncpy(UnQuoted, SQUID_ERROR_TOKEN, sizeof(UnQuoted));
197  else {
198  debugs(3, DBG_CRITICAL, "FATAL: " << errorStr << ": " << errorPos);
199  self_destruct();
200  }
201  }
202 
203  if (next)
204  *next = s + 1;
205  return UnQuoted;
206 }
207 
208 void
210 {
211  CfgLine = line;
212  CfgPos = line;
213  while (!CfgLineTokens_.empty()) {
214  char *token = CfgLineTokens_.front();
215  CfgLineTokens_.pop();
216  free(token);
217  }
218 }
219 
220 SBuf
222 {
224 }
225 
226 char *
227 ConfigParser::TokenParse(const char * &nextToken, ConfigParser::TokenType &type)
228 {
229  if (!nextToken || *nextToken == '\0')
230  return nullptr;
232  nextToken += strspn(nextToken, w_space);
233 
234  if (*nextToken == '#')
235  return nullptr;
236 
237  if (ConfigParser::RecognizeQuotedValues && (*nextToken == '"' || *nextToken == '\'')) {
239  char *token = xstrdup(UnQuote(nextToken, &nextToken));
240  CfgLineTokens_.push(token);
241  return token;
242  }
243 
244  const char *tokenStart = nextToken;
245  const char *sep;
248  sep = "=";
249  else
250  sep = w_space;
252  sep = "\n";
254  sep = w_space "\\";
255  else if (!ConfigParser::RecognizeQuotedValues || *nextToken == '(')
256  sep = w_space;
257  else
258  sep = w_space "(";
259  nextToken += strcspn(nextToken, sep);
260 
261  while (ConfigParser::RecognizeQuotedPair_ && *nextToken == '\\') {
262  // NP: do not permit \0 terminator to be escaped.
263  if (*(nextToken+1) && *(nextToken+1) != '\r' && *(nextToken+1) != '\n') {
264  nextToken += 2; // skip the quoted-pair (\-escaped) character
265  nextToken += strcspn(nextToken, sep);
266  } else {
267  debugs(3, DBG_CRITICAL, "FATAL: Unescaped '\' character in regex pattern: " << tokenStart);
268  self_destruct();
269  }
270  }
271 
272  if (ConfigParser::RecognizeQuotedValues && *nextToken == '(') {
273  if (strncmp(tokenStart, "parameters", nextToken - tokenStart) == 0)
275  else {
276  if (PreviewMode_) {
277  char *err = xstrdup(SQUID_ERROR_TOKEN);
278  CfgLineTokens_.push(err);
279  return err;
280  } else {
281  debugs(3, DBG_CRITICAL, "FATAL: Unknown cfg function: " << tokenStart);
282  self_destruct();
283  }
284  }
285  } else
287 
288  char *token = nullptr;
289  if (nextToken - tokenStart) {
291  bool tokenIsNumber = true;
292  for (const char *s = tokenStart; s != nextToken; ++s) {
293  const bool isValidChar = isalnum(*s) || strchr(".,()-=_/:+", *s) ||
294  (tokenIsNumber && *s == '%' && (s + 1 == nextToken));
295 
296  if (!isdigit(*s))
297  tokenIsNumber = false;
298 
299  if (!isValidChar) {
300  if (PreviewMode_) {
301  char *err = xstrdup(SQUID_ERROR_TOKEN);
302  CfgLineTokens_.push(err);
303  return err;
304  } else {
305  debugs(3, DBG_CRITICAL, "FATAL: Not alphanumeric character '"<< *s << "' in unquoted token " << tokenStart);
306  self_destruct();
307  }
308  }
309  }
310  }
311  token = xstrndup(tokenStart, nextToken - tokenStart + 1);
312  CfgLineTokens_.push(token);
313  }
314 
315  if (*nextToken != '\0' && *nextToken != '#') {
316  ++nextToken;
317  }
318 
319  return token;
320 }
321 
322 char *
324 {
325  const char *pos = CfgPos;
326  char *token = TokenParse(pos, type);
327  // If not in preview mode the next call of this method should start
328  // parsing after the end of current token.
329  // For function "parameters(...)" we need always to update current parsing
330  // position to allow parser read the arguments of "parameters(..)"
331  if (!PreviewMode_ || type == FunctionParameters)
332  CfgPos = pos;
333  // else next call will read the same token
334  return token;
335 }
336 
337 char *
339 {
340  char *token = nullptr;
341 
342  do {
343  while (token == nullptr && !CfgFiles.empty()) {
344  ConfigParser::CfgFile *wordfile = CfgFiles.top();
345  token = wordfile->parse(LastTokenType);
346  if (!token) {
347  assert(!wordfile->isOpen());
348  CfgFiles.pop();
349  debugs(3, 4, "CfgFiles.pop " << wordfile->filePath);
350  delete wordfile;
351  }
352  }
353 
354  if (!token)
356 
358  //Disable temporary preview mode, we need to parse function parameters
359  const bool savePreview = ConfigParser::PreviewMode_;
361 
362  char *path = NextToken();
364  debugs(3, DBG_CRITICAL, "FATAL: Quoted filename missing: " << token);
365  self_destruct();
366  return nullptr;
367  }
368 
369  // The next token in current cfg file line must be a ")"
370  char *end = NextToken();
371  ConfigParser::PreviewMode_ = savePreview;
372  if (LastTokenType != ConfigParser::SimpleToken || strcmp(end, ")") != 0) {
373  debugs(3, DBG_CRITICAL, "FATAL: missing ')' after " << token << "(\"" << path << "\"");
374  self_destruct();
375  return nullptr;
376  }
377 
378  if (CfgFiles.size() > 16) {
379  debugs(3, DBG_CRITICAL, "FATAL: can't open %s for reading parameters: includes are nested too deeply (>16)!\n" << path);
380  self_destruct();
381  return nullptr;
382  }
383 
385  if (!path || !wordfile->startParse(path)) {
386  debugs(3, DBG_CRITICAL, "FATAL: Error opening config file: " << token);
387  delete wordfile;
388  self_destruct();
389  return nullptr;
390  }
391  CfgFiles.push(wordfile);
392  token = nullptr;
393  }
394  } while (token == nullptr && !CfgFiles.empty());
395 
396  return token;
397 }
398 
399 char *
401 {
402  PreviewMode_ = true;
403  char *token = NextToken();
404  PreviewMode_ = false;
405  return token;
406 }
407 
408 char *
410 {
411  ParseQuotedOrToEol_ = true;
412  char *token = NextToken();
413  ParseQuotedOrToEol_ = false;
414 
415  // Assume end of current config line
416  // Close all open configuration files for this config line
417  while (!CfgFiles.empty()) {
418  ConfigParser::CfgFile *wordfile = CfgFiles.top();
419  CfgFiles.pop();
420  delete wordfile;
421  }
422 
423  return token;
424 }
425 
426 bool
427 ConfigParser::optionalKvPair(char * &key, char * &value)
428 {
429  key = nullptr;
430  value = nullptr;
431 
432  if (const char *currentToken = PeekAtToken()) {
433  // NextKvPair() accepts "a = b" and skips "=" or "a=". To avoid
434  // misinterpreting the admin intent, we use strict checks.
435  if (const auto middle = strchr(currentToken, '=')) {
436  if (middle == currentToken)
437  throw TextException(ToSBuf("missing key in a key=value option: ", currentToken), Here());
438  if (middle + 1 == currentToken + strlen(currentToken))
439  throw TextException(ToSBuf("missing value in a key=value option: ", currentToken), Here());
440  } else
441  return false; // not a key=value token
442 
443  if (!NextKvPair(key, value)) // may still fail (e.g., bad value quoting)
444  throw TextException(ToSBuf("invalid key=value option: ", currentToken), Here());
445 
446  return true;
447  }
448 
449  return false; // end of directive or input
450 }
451 
452 bool
453 ConfigParser::NextKvPair(char * &key, char * &value)
454 {
455  key = value = nullptr;
456  ParseKvPair_ = true;
458  if ((key = NextToken()) != nullptr) {
460  value = NextQuotedToken();
461  }
462  ParseKvPair_ = false;
463 
464  if (!key)
465  return false;
466  if (!value) {
467  debugs(3, DBG_CRITICAL, "ERROR: Failure while parsing key=value token. Value missing after: " << key);
468  return false;
469  }
470 
471  return true;
472 }
473 
474 char *
476 {
478  debugs(3, DBG_CRITICAL, "FATAL: Can not read regex expression while configuration_includes_quoted_values is enabled");
479  self_destruct();
480  }
482  char * token = strtokFile();
484  return token;
485 }
486 
487 std::unique_ptr<RegexPattern>
488 ConfigParser::regex(const char *expectedRegexDescription)
489 {
491  throw TextException("Cannot read regex expression while configuration_includes_quoted_values is enabled", Here());
492 
493  SBuf pattern;
494  int flags = REG_EXTENDED | REG_NOSUB;
495 
497  const auto flagOrPattern = token(expectedRegexDescription);
498  if (flagOrPattern.cmp("-i") == 0) {
499  flags |= REG_ICASE;
500  pattern = token(expectedRegexDescription);
501  } else if (flagOrPattern.cmp("+i") == 0) {
502  flags &= ~REG_ICASE;
503  pattern = token(expectedRegexDescription);
504  } else {
505  pattern = flagOrPattern;
506  }
508 
509  return std::unique_ptr<RegexPattern>(new RegexPattern(pattern, flags));
510 }
511 
512 CachePeer &
513 ConfigParser::cachePeer(const char *peerNameTokenDescription)
514 {
515  if (const auto name = NextToken()) {
516  debugs(3, 5, CurrentLocation() << ' ' << peerNameTokenDescription << ": " << name);
517 
518  if (const auto p = findCachePeerByName(name))
519  return *p;
520 
521  throw TextException(ToSBuf("Cannot find a previously declared cache_peer referred to by ",
522  peerNameTokenDescription, " as ", name), Here());
523  }
524 
525  throw TextException(ToSBuf("Missing ", peerNameTokenDescription), Here());
526 }
527 
528 char *
530 {
531  const bool saveRecognizeQuotedValues = ConfigParser::RecognizeQuotedValues;
533  char *token = NextToken();
534  ConfigParser::RecognizeQuotedValues = saveRecognizeQuotedValues;
535  return token;
536 }
537 
538 const char *
540 {
541  static String quotedStr;
542  const char *s = var.termedBuf();
543  bool needQuote = false;
544 
545  for (const char *l = s; !needQuote && *l != '\0'; ++l )
546  needQuote = !isalnum(*l);
547 
548  if (!needQuote)
549  return s;
550 
551  quotedStr.clean();
552  quotedStr.append('"');
553  for (; *s != '\0'; ++s) {
554  if (*s == '"' || *s == '\\')
555  quotedStr.append('\\');
556  quotedStr.append(*s);
557  }
558  quotedStr.append('"');
559  return quotedStr.termedBuf();
560 }
561 
562 void
564 {
566  throw TextException("duplicate configuration directive", Here());
567 }
568 
569 void
571 {
573  if (const auto garbage = PeekAtToken())
574  throw TextException(ToSBuf("trailing garbage at the end of a configuration directive: ", garbage), Here());
575  // TODO: cfg_directive = nullptr; // currently in generated code
576 }
577 
578 SBuf
579 ConfigParser::token(const char *expectedTokenDescription)
580 {
581  if (const auto extractedToken = NextToken()) {
582  debugs(3, 5, CurrentLocation() << ' ' << expectedTokenDescription << ": " << extractedToken);
583  return SBuf(extractedToken);
584  }
585  throw TextException(ToSBuf("missing ", expectedTokenDescription), Here());
586 }
587 
588 bool
589 ConfigParser::skipOptional(const char *keyword)
590 {
591  assert(keyword);
592  if (const auto nextToken = PeekAtToken()) {
593  if (strcmp(nextToken, keyword) == 0) {
594  (void)NextToken();
595  return true;
596  }
597  return false; // the next token on the line is not the optional keyword
598  }
599  return false; // no more tokens (i.e. we are at the end of the line)
600 }
601 
602 ACLList *
604 {
605  if (!skipOptional("if"))
606  return nullptr; // OK: the directive has no ACLs
607 
608  ACLList *acls = nullptr;
609  const auto aclCount = aclParseAclList(*this, &acls, cfg_directive);
610  assert(acls);
611  if (aclCount <= 0)
612  throw TextException("missing ACL name(s) after 'if' keyword", Here());
613  return acls;
614 }
615 
616 bool
618 {
619  assert(wordFile == nullptr);
620  debugs(3, 3, "Parsing from " << path);
621  if ((wordFile = fopen(path, "r")) == nullptr) {
622  debugs(3, DBG_CRITICAL, "WARNING: file :" << path << " not found");
623  return false;
624  }
625 
626 #if _SQUID_WINDOWS_
627  setmode(fileno(wordFile), O_TEXT);
628 #endif
629 
630  filePath = path;
631  return getFileLine();
632 }
633 
634 bool
636 {
637  // Else get the next line
638  if (fgets(parseBuffer, CONFIG_LINE_LIMIT, wordFile) == nullptr) {
639  /* stop reading from file */
640  fclose(wordFile);
641  wordFile = nullptr;
642  parseBuffer[0] = '\0';
643  return false;
644  }
645  parsePos = parseBuffer;
646  currentLine = parseBuffer;
647  lineNo++;
648  return true;
649 }
650 
651 char *
653 {
654  if (!wordFile)
655  return nullptr;
656 
657  if (!*parseBuffer)
658  return nullptr;
659 
660  char *token;
661  while (!(token = nextElement(type))) {
662  if (!getFileLine())
663  return nullptr;
664  }
665  return token;
666 }
667 
668 char *
670 {
671  const char *pos = parsePos;
672  char *token = TokenParse(pos, type);
673  if (!PreviewMode_ || type == FunctionParameters)
674  parsePos = pos;
675  // else next call will read the same token;
676  return token;
677 }
678 
680 {
681  if (wordFile)
682  fclose(wordFile);
683 }
684 
ACLList * optionalAclList()
parses an [if [!]<acl>...] construct
#define Here()
source code location of the caller
Definition: Here.h:15
static SBuf CurrentLocation()
#define DBG_CRITICAL
Definition: Stream.h:37
static char * NextQuotedToken()
static char * strtokFile()
Definition: ConfigParser.cc:65
#define LOCAL_ARRAY(type, name, size)
Definition: squid.h:62
static bool RecognizeQuotedPair_
The next tokens may contain quoted-pair (-escaped) characters.
Definition: ConfigParser.h:229
#define O_TEXT
Definition: defines.h:131
static bool PreviewMode_
Definition: ConfigParser.h:230
static bool StrictMode
Definition: ConfigParser.h:160
FILE * wordFile
Pointer to the file.
Definition: ConfigParser.h:196
Definition: SBuf.h:93
static bool RecognizeQuotedValues
configuration_includes_quoted_values in squid.conf
Definition: ConfigParser.h:152
static char * PeekAtToken()
#define xstrdup
char * xstrncpy(char *dst, const char *src, size_t n)
Definition: xstring.cc:37
static char * UnQuote(const char *token, const char **next=nullptr)
static bool AllowMacros_
Definition: ConfigParser.h:227
char * parse(TokenType &type)
std::unique_ptr< RegexPattern > regex(const char *expectedRegexDescription)
extracts and returns a regex (including any optional flags)
std::string filePath
The file path.
Definition: ConfigParser.h:200
#define w_space
void self_destruct(void)
Definition: cache_cf.cc:275
bool getFileLine()
Read the next line from the file.
size_t aclParseAclList(ConfigParser &, ACLList **config, const char *label)
Definition: Gadgets.cc:184
#define CONFIG_LINE_LIMIT
Definition: ConfigParser.h:33
static TokenType LastTokenType
The type of last parsed element.
Definition: ConfigParser.h:223
CachePeer * findCachePeerByName(const char *const name)
cache_peer with a given name (or nil)
Definition: neighbors.cc:1048
void append(char const *buf, int len)
Definition: String.cc:131
static std::queue< char * > CfgLineTokens_
Store the list of tokens for current configuration line.
Definition: ConfigParser.h:226
static bool ParseQuotedOrToEol_
The next tokens will be handled as quoted or to_eol token.
Definition: ConfigParser.h:228
static bool ParseKvPair_
The next token will be handled as kv-pair token.
Definition: ConfigParser.h:231
SBuf token(const char *expectedTokenDescription)
extracts and returns a required token
void rejectDuplicateDirective()
rejects configuration due to a repeated directive
void destruct()
Definition: ConfigParser.cc:38
bool optionalKvPair(char *&key, char *&value)
#define assert(EX)
Definition: assert.h:17
char * nextElement(TokenType &type)
const char * cfg_filename
Definition: cache_cf.cc:270
void fatalf(const char *fmt,...)
Definition: fatal.cc:68
CachePeer & cachePeer(const char *peerNameTokenDescription)
extracts a cache_peer name token and returns the corresponding CachePeer
static char * RegexStrtokFile()
static bool NextKvPair(char *&key, char *&value)
int config_lineno
Definition: cache_cf.cc:271
void closeDirective()
stops parsing the current configuration directive
static const char * SQUID_ERROR_TOKEN
Definition: ConfigParser.cc:35
char * xstrndup(const char *s, size_t n)
Definition: xstring.cc:56
static char * NextToken()
static const char * QuoteString(const String &var)
static char * NextElement(TokenType &type)
Wrapper method for TokenParse.
static const char * CfgLine
The current line to parse.
Definition: ConfigParser.h:224
bool skipOptional(const char *keyword)
either extracts the given (optional) token or returns false
const char * termedBuf() const
Definition: SquidString.h:93
static char * NextQuotedOrToEol()
a source code location that is cheap to create, copy, and store
Definition: Here.h:29
an std::runtime_error with thrower location info
Definition: TextException.h:20
static void SetCfgLine(char *line)
Set the configuration file line to parse.
SBuf ToSBuf(Args &&... args)
slowly stream-prints all arguments into a freshly allocated SBuf
Definition: Stream.h:63
static enum ConfigParser::ParsingStates KvPairState_
Parsing state while parsing kv-pair tokens.
Definition: ConfigParser.cc:31
static const char * CfgPos
Pointer to the next element in cfgLine string.
Definition: ConfigParser.h:225
int shutting_down
std::string currentLine
The current line to parse.
Definition: ConfigParser.h:201
const char * cfg_directive
During parsing, the name of the current squid.conf directive being parsed.
Definition: cache_cf.cc:269
static std::stack< CfgFile * > CfgFiles
The stack of open cfg files.
Definition: ConfigParser.h:222
char config_input_line[BUFSIZ]
Definition: cache_cf.cc:272
#define debugs(SECTION, LEVEL, CONTENT)
Definition: Stream.h:192
bool startParse(char *path)
int lineNo
Current line number.
Definition: ConfigParser.h:202
static char * TokenParse(const char *&nextToken, TokenType &type)
void clean()
Definition: String.cc:104
bool isOpen()
True if the configuration file is open.
Definition: ConfigParser.h:173

 

Introduction

Documentation

Support

Miscellaneous