1/*
2 * LegacyClonk
3 *
4 * Copyright (c) RedWolf Design
5 * Copyright (c) 2007, Sven2
6 * Copyright (c) 2017-2021, The LegacyClonk Team and contributors
7 *
8 * Distributed under the terms of the ISC license; see accompanying file
9 * "COPYING" for details.
10 *
11 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
12 * See accompanying file "TRADEMARK" for details.
13 *
14 * To redistribute this file separately, substitute the full license texts
15 * for the above references.
16 */
17
18// user input validation functions
19
20#include <C4InputValidation.h>
21#include <C4Config.h>
22
23#ifdef C4ENGINE
24#include "StdMarkup.h"
25#endif
26
27namespace C4InVal
28{
29
30bool ValidateString(char *szString, ValidationOption eOption, size_t iMaxSize)
31{
32 // validate in a StdStrBuf. Does one alloc and copy :(
33 StdStrBuf buf; buf.Copy(pnData: szString);
34 bool fInvalid = ValidateString(rsString&: buf, eOption);
35 if (fInvalid) SCopy(szSource: buf.getData(), sTarget: szString, iMaxL: iMaxSize);
36 return fInvalid;
37}
38
39bool ValidateString(StdStrBuf &rsString, ValidationOption eOption)
40{
41 bool fValid = true;
42 // validation depending on option
43 // check min length
44 if (!rsString.getLength())
45 {
46 // empty if not allowed?
47#ifdef C4ENGINE
48 if (eOption != VAL_NameAllowEmpty && eOption != VAL_NameExAllowEmpty && eOption != VAL_Comment)
49 {
50#endif
51 rsString.Copy(pnData: "empty");
52 fValid = false;
53#ifdef C4ENGINE
54 }
55#endif
56 }
57 switch (eOption)
58 {
59 case VAL_Filename: // regular filenames only
60 // absolutely no directory traversal
61 if (rsString.ReplaceChar(cOld: '/', cNew: '_')) fValid = false;
62 if (rsString.ReplaceChar(cOld: '\\', cNew: '_')) fValid = false;
63
64 // fallthrough to general file name validation
65 case VAL_SubPathFilename: // filenames and optional subpath
66 // do not traverse upwards in file hierarchy
67 if (rsString.Replace(szOld: "..", szNew: "__")) fValid = false;
68 if (*rsString.getData() == '/' || *rsString.getData() == '\\') { *rsString.getMData() = '_'; fValid = false; }
69
70 // fallthrough to general file name validation
71 case VAL_FullPath: // full filename paths
72 // some characters are prohibited in filenames in general
73 if (rsString.ReplaceChar(cOld: '*', cNew: '_')) fValid = false;
74 if (rsString.ReplaceChar(cOld: '?', cNew: '_')) fValid = false;
75 if (rsString.ReplaceChar(cOld: '<', cNew: '_')) fValid = false;
76 if (rsString.ReplaceChar(cOld: '>', cNew: '_')) fValid = false;
77 // ';' and '|' is never allowed in filenames, because it would cause problems in many engine internal file lists
78 if (rsString.ReplaceChar(cOld: ';', cNew: '_')) fValid = false;
79 if (rsString.ReplaceChar(cOld: '|', cNew: '_')) fValid = false;
80 // the colon is generally prohibited except at pos 2 (C:\...), because it could lead to creation of (invisible) streams on NTFS
81 if (rsString.ReplaceChar(cOld: ':', cNew: '_', iStartSearch: 2)) fValid = false;
82 if (*rsString.getData() == ':') { *rsString.getMData() = '_'; fValid = false; }
83 // validate drive letter
84 if (rsString.getLength() >= 2 && *rsString.getPtr(i: 1) == ':')
85 {
86 if (eOption != VAL_FullPath)
87 {
88 *rsString.getMPtr(i: 1) = '_'; fValid = false;
89 }
90 else if (!isalpha(static_cast<unsigned char>(*rsString.getData())) || (*rsString.getPtr(i: 2) != '\\' && *rsString.getPtr(i: 2) != '/'))
91 {
92 *rsString.getMData() = *rsString.getMPtr(i: 1) = '_'; fValid = false;
93 }
94 }
95 break;
96
97#ifdef C4ENGINE
98 case VAL_NameNoEmpty:
99 case VAL_NameAllowEmpty:
100 // excess '{' will break the team chat, remove
101 {
102 std::string str{rsString.getData()};
103 if (const auto it = std::remove(first: str.begin(), last: str.end(), value: '{'); it != str.end())
104 {
105 fValid = false;
106 str.erase(first: it, last: str.end());
107 rsString.Copy(pnData: str.c_str());
108 }
109 }
110 // no markup
111 if (CMarkup::StripMarkup(sText: &rsString)) { fValid = false; }
112 // trim spaces
113 if (rsString.TrimSpaces()) fValid = false;
114 // min length
115 if (eOption == VAL_NameNoEmpty) if (!rsString.getLength()) { fValid = false; rsString.Copy(pnData: "Unknown"); }
116 // max length
117 if (rsString.getLength() > C4MaxName) { fValid = false; rsString.SetLength(C4MaxName); }
118 break;
119
120 case VAL_NameExNoEmpty:
121 case VAL_NameExAllowEmpty:
122 // trim spaces
123 if (rsString.TrimSpaces()) fValid = false;
124 // min length
125 if (eOption == VAL_NameExNoEmpty) if (!rsString.getLength()) { fValid = false; rsString.Copy(pnData: "Unknown"); }
126 // max length
127 if (rsString.getLength() > C4MaxLongName) { fValid = false; rsString.SetLength(C4MaxLongName); }
128 break;
129
130 case VAL_IRCName: // nickname for IRC. a-z, A-Z, _^{[]} only; 0-9|- inbetween; max 30 characters
131 if (rsString.getLength() > 30) fValid = false;
132 if (rsString.getLength() < 2) fValid = false;
133 if (!rsString.ValidateChars(szInitialChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_^{[]}", szMidChars: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_^{[]}0123456789|-")) { fValid = false; rsString.Copy(pnData: "Guest"); }
134 if (SEqualNoCase(szStr1: rsString.getData(), szStr2: "NickServ")
135 || SEqualNoCase(szStr1: rsString.getData(), szStr2: "ChanServ")
136 || SEqualNoCase(szStr1: rsString.getData(), szStr2: "MemoServ")
137 || SEqualNoCase(szStr1: rsString.getData(), szStr2: "OperServ")
138 || SEqualNoCase(szStr1: rsString.getData(), szStr2: "HelpServ")) fValid = false;
139 if (!fValid) rsString.Copy(pnData: "Guest");
140 break;
141
142 case VAL_IRCPass: // password for IRC; max 31 characters
143 // max length; no spaces
144 if (rsString.getLength() > 31) { fValid = false; rsString.SetLength(31); }
145 if (rsString.getLength() < 2) { fValid = false; rsString.Copy(pnData: "secret"); }
146 if (rsString.ReplaceChar(cOld: ' ', cNew: '_')) fValid = false;
147 break;
148
149 case VAL_IRCChannel: // IRC channel name
150 if (rsString.getLength() > 32) { fValid = false; rsString.SetLength(32); }
151 else if (rsString.getLength() < 2) { fValid = false; rsString.Copy(pnData: "#clonken"); }
152 else if (*rsString.getData() != '#' && *rsString.getData() != '+') { fValid = false; *rsString.getMData() = '#'; }
153 if (rsString.ReplaceChar(cOld: ' ', cNew: '_')) fValid = false;
154 break;
155
156 case VAL_Comment: // comment - just limit length
157 if (rsString.getLength() > C4MaxComment) { fValid = false; rsString.SetLength(C4MaxComment); }
158 break;
159#endif
160
161 default:
162 assert(!"not yet implemented");
163 }
164 // issue warning for invalid adjustments
165 if (!fValid)
166 {
167 const char *szOption = "unknown";
168 switch (eOption)
169 {
170 case VAL_Filename: szOption = "filename"; break;
171 case VAL_SubPathFilename: szOption = "(sub-)filename"; break;
172 case VAL_FullPath: szOption = "free filename"; break;
173#ifdef C4ENGINE
174 case VAL_NameNoEmpty: szOption = "strict name"; break;
175 case VAL_NameExNoEmpty: szOption = "name"; break;
176 case VAL_NameAllowEmpty: szOption = "strict name*"; break;
177 case VAL_NameExAllowEmpty: szOption = "name*"; break;
178 case VAL_IRCName: szOption = "IRC nick"; break;
179 case VAL_IRCPass: szOption = "IRC password"; break;
180 case VAL_IRCChannel: szOption = "IRC channel"; break;
181 case VAL_Comment: szOption = "Comment"; break;
182#endif
183 }
184 }
185 return !fValid;
186}
187
188}
189