|
1
|
1 #include "pfc-lite.h"
|
|
|
2 #include "pathUtils.h"
|
|
|
3
|
|
|
4 static_assert(L'Ö' == 0xD6, "Compile as Unicode!!!");
|
|
|
5
|
|
|
6 namespace pfc { namespace io { namespace path {
|
|
|
7
|
|
|
8 #ifdef _WINDOWS
|
|
|
9 #define KPathSeparators "\\/|"
|
|
|
10 #else
|
|
|
11 #define KPathSeparators "/"
|
|
|
12 #endif
|
|
|
13
|
|
|
14 string getFileName(string path) {
|
|
|
15 t_size split = path.lastIndexOfAnyChar(KPathSeparators);
|
|
|
16 if (split == SIZE_MAX) return path;
|
|
|
17 else return path.subString(split+1);
|
|
|
18 }
|
|
|
19 string getFileNameWithoutExtension(string path) {
|
|
|
20 string fn = getFileName(path);
|
|
|
21 t_size split = fn.lastIndexOf('.');
|
|
|
22 if (split == SIZE_MAX) return fn;
|
|
|
23 else return fn.subString(0,split);
|
|
|
24 }
|
|
|
25 string getFileExtension(string path) {
|
|
|
26 string fn = getFileName(path);
|
|
|
27 t_size split = fn.lastIndexOf('.');
|
|
|
28 if (split == SIZE_MAX) return "";
|
|
|
29 else return fn.subString(split);
|
|
|
30 }
|
|
|
31 string getDirectory(string filePath) {return getParent(filePath);}
|
|
|
32
|
|
|
33 string getParent(string filePath) {
|
|
|
34 t_size split = filePath.lastIndexOfAnyChar(KPathSeparators);
|
|
|
35 if (split == SIZE_MAX) return "";
|
|
|
36 #ifdef _WINDOWS
|
|
|
37 if (split > 0 && getIllegalNameChars().contains(filePath[split-1])) {
|
|
|
38 if (split + 1 < filePath.length()) return filePath.subString(0,split+1);
|
|
|
39 else return "";
|
|
|
40 }
|
|
|
41 #endif
|
|
|
42 return filePath.subString(0,split);
|
|
|
43 }
|
|
|
44 string combine(string basePath,string fileName) {
|
|
|
45 if (basePath.length() > 0) {
|
|
|
46 if (!isSeparator(basePath.lastChar())) {
|
|
|
47 basePath.add_byte( getDefaultSeparator() );
|
|
|
48 }
|
|
|
49 return basePath + fileName;
|
|
|
50 } else {
|
|
|
51 //todo?
|
|
|
52 return fileName;
|
|
|
53 }
|
|
|
54 }
|
|
|
55
|
|
|
56 bool isSeparator(char c) {
|
|
|
57 return strchr(KPathSeparators, c) != nullptr;
|
|
|
58 }
|
|
|
59 string getSeparators() {
|
|
|
60 return KPathSeparators;
|
|
|
61 }
|
|
|
62
|
|
|
63 const char * charReplaceDefault(char c) {
|
|
|
64 switch (c) {
|
|
|
65 case '*':
|
|
|
66 return "x";
|
|
|
67 case '\"':
|
|
|
68 return "\'\'";
|
|
|
69 case ':':
|
|
|
70 case '/':
|
|
|
71 case '\\':
|
|
|
72 return "-";
|
|
|
73 case '?':
|
|
|
74 return "";
|
|
|
75 default:
|
|
|
76 return "_";
|
|
|
77 }
|
|
|
78 }
|
|
|
79
|
|
|
80 const char * charReplaceModern(char c) {
|
|
|
81 switch (c) {
|
|
|
82 case '*':
|
|
|
83 return reinterpret_cast<const char*>( u8"∗" );
|
|
|
84 case '\"':
|
|
|
85 return reinterpret_cast<const char*>( u8"''" );
|
|
|
86 case ':':
|
|
|
87 return reinterpret_cast<const char*>( u8"∶" );
|
|
|
88 case '/':
|
|
|
89 return reinterpret_cast<const char*>( u8"\u2215" );
|
|
|
90 case '\\':
|
|
|
91 return reinterpret_cast<const char*>( u8"⧵" );
|
|
|
92 case '?':
|
|
|
93 return reinterpret_cast<const char*>( u8"?" );
|
|
|
94 case '<':
|
|
|
95 return reinterpret_cast<const char*>( u8"˂" );
|
|
|
96 case '>':
|
|
|
97 return reinterpret_cast<const char*>( u8"˃" );
|
|
|
98 case '|':
|
|
|
99 return reinterpret_cast<const char*>( u8"∣" );
|
|
|
100 default:
|
|
|
101 return "_";
|
|
|
102 }
|
|
|
103 }
|
|
|
104
|
|
|
105 string replaceIllegalPathChars(string fn, charReplace_t replaceIllegalChar) {
|
|
|
106 string illegal = getIllegalNameChars();
|
|
|
107 string separators = getSeparators();
|
|
|
108 string_formatter output;
|
|
|
109 for(t_size walk = 0; walk < fn.length(); ++walk) {
|
|
|
110 const char c = fn[walk];
|
|
|
111 if (separators.contains(c)) {
|
|
|
112 output.add_byte(getDefaultSeparator());
|
|
|
113 } else if (string::isNonTextChar(c) || illegal.contains(c)) {
|
|
|
114 string replacement = replaceIllegalChar(c);
|
|
|
115 if (replacement.containsAnyChar(illegal)) /*per-OS weirdness security*/ replacement = "_";
|
|
|
116 output << replacement.ptr();
|
|
|
117 } else {
|
|
|
118 output.add_byte(c);
|
|
|
119 }
|
|
|
120 }
|
|
|
121 return output.toString();
|
|
|
122 }
|
|
|
123
|
|
|
124 string replaceIllegalNameChars(string fn, bool allowWC, charReplace_t replaceIllegalChar) {
|
|
|
125 const string illegal = getIllegalNameChars(allowWC);
|
|
|
126 string_formatter output;
|
|
|
127 for(t_size walk = 0; walk < fn.length(); ++walk) {
|
|
|
128 const char c = fn[walk];
|
|
|
129 if (string::isNonTextChar(c) || illegal.contains(c)) {
|
|
|
130 string replacement = replaceIllegalChar(c);
|
|
|
131 if (replacement.containsAnyChar(illegal)) /*per-OS weirdness security*/ replacement = "_";
|
|
|
132 output << replacement.ptr();
|
|
|
133 } else {
|
|
|
134 output.add_byte(c);
|
|
|
135 }
|
|
|
136 }
|
|
|
137 return output.toString();
|
|
|
138 }
|
|
|
139
|
|
|
140 bool isInsideDirectory(pfc::string directory, pfc::string inside) {
|
|
|
141 //not very efficient
|
|
|
142 string walk = inside;
|
|
|
143 for(;;) {
|
|
|
144 walk = getParent(walk);
|
|
|
145 if (walk == "") return false;
|
|
|
146 if (equals(directory,walk)) return true;
|
|
|
147 }
|
|
|
148 }
|
|
|
149 bool isDirectoryRoot(string path) {
|
|
|
150 return getParent(path).isEmpty();
|
|
|
151 }
|
|
|
152 //OS-dependant part starts here
|
|
|
153
|
|
|
154
|
|
|
155 char getDefaultSeparator() {
|
|
|
156 #ifdef _WINDOWS
|
|
|
157 return '\\';
|
|
|
158 #else
|
|
|
159 return '/';
|
|
|
160 #endif
|
|
|
161 }
|
|
|
162
|
|
|
163 #ifdef _WINDOWS
|
|
|
164 #define KIllegalNameCharsEx ":<>\""
|
|
|
165 #else
|
|
|
166 // Mac OS allows : in filenames but does funny things presenting them in Finder, so don't use it
|
|
|
167 #define KIllegalNameCharsEx ":"
|
|
|
168 #endif
|
|
|
169
|
|
|
170 #define KWildcardChars "*?"
|
|
|
171
|
|
|
172 #define KIllegalNameChars KPathSeparators KIllegalNameCharsEx KWildcardChars
|
|
|
173 #define KIllegalNameChars_noWC KPathSeparators KIllegalNameCharsEx
|
|
|
174
|
|
|
175 static string g_illegalNameChars ( KIllegalNameChars );
|
|
|
176 static string g_illegalNameChars_noWC ( KIllegalNameChars_noWC );
|
|
|
177
|
|
|
178 string getIllegalNameChars(bool allowWC) {
|
|
|
179 return allowWC ? g_illegalNameChars_noWC : g_illegalNameChars;
|
|
|
180 }
|
|
|
181
|
|
|
182 #ifdef _WINDOWS
|
|
|
183 static const char * const specialIllegalNames[] = {
|
|
|
184 "con", "aux", "lst", "prn", "nul", "eof", "inp", "out"
|
|
|
185 };
|
|
|
186 #endif // _WINDOWS
|
|
|
187
|
|
|
188 #ifdef _WIN32
|
|
|
189 static constexpr unsigned maxPathComponent = 255;
|
|
|
190 #else
|
|
|
191 static constexpr unsigned maxPathComponent = NAME_MAX;
|
|
|
192 #endif
|
|
|
193
|
|
|
194 static size_t safeTruncat( const char * str, size_t maxLen ) {
|
|
|
195 size_t i = 0;
|
|
|
196 size_t ret = 0;
|
|
|
197 for( ; i < maxLen; ++ i ) {
|
|
|
198 auto d = pfc::utf8_char_len( str + ret );
|
|
|
199 if ( d == 0 ) break;
|
|
|
200 ret += d;
|
|
|
201 }
|
|
|
202 return ret;
|
|
|
203 }
|
|
|
204
|
|
|
205 static size_t utf8_length( const char * str ) {
|
|
|
206 size_t ret = 0;
|
|
|
207 for (; ++ret;) {
|
|
|
208 size_t d = pfc::utf8_char_len( str );
|
|
|
209 if ( d == 0 ) break;
|
|
|
210 str += d;
|
|
|
211 }
|
|
|
212 return ret;
|
|
|
213 }
|
|
|
214 static string truncatePathComponent( string name, bool preserveExt ) {
|
|
|
215
|
|
|
216 if (name.length() <= maxPathComponent) return name;
|
|
|
217 if (preserveExt) {
|
|
|
218 auto dot = name.lastIndexOf('.');
|
|
|
219 if (dot != pfc_infinite) {
|
|
|
220 const auto ext = name.subString(dot);
|
|
|
221 const auto extLen = utf8_length( ext.c_str() );
|
|
|
222 if (extLen < maxPathComponent) {
|
|
|
223 auto lim = maxPathComponent - extLen;
|
|
|
224 lim = safeTruncat( name.c_str(), lim );
|
|
|
225 if (lim < dot) {
|
|
|
226 return name.subString(0, lim) + ext;
|
|
|
227 }
|
|
|
228 }
|
|
|
229 }
|
|
|
230 }
|
|
|
231
|
|
|
232 size_t truncat = safeTruncat( name.c_str(), maxPathComponent );
|
|
|
233 return name.subString(0, truncat);
|
|
|
234 }
|
|
|
235
|
|
|
236 static string trailingSanity(string name, bool preserveExt, const char * lstIllegal) {
|
|
|
237
|
|
|
238 const auto isIllegalTrailingChar = [lstIllegal](char c) {
|
|
|
239 return strchr(lstIllegal, c) != nullptr;
|
|
|
240 };
|
|
|
241
|
|
|
242 t_size end = name.length();
|
|
|
243 if (preserveExt) {
|
|
|
244 size_t offset = pfc::string_find_last(name.c_str(), '.');
|
|
|
245 if (offset < end) end = offset;
|
|
|
246 }
|
|
|
247 const size_t endEx = end;
|
|
|
248 while (end > 0) {
|
|
|
249 if (!isIllegalTrailingChar(name[end - 1])) break;
|
|
|
250 --end;
|
|
|
251 }
|
|
|
252 t_size begin = 0;
|
|
|
253 while (begin < end) {
|
|
|
254 if (!isIllegalTrailingChar(name[begin])) break;
|
|
|
255 ++begin;
|
|
|
256 }
|
|
|
257 if (end < endEx || begin > 0) {
|
|
|
258 name = name.subString(begin, end - begin) + name.subString(endEx);
|
|
|
259 }
|
|
|
260 return name;
|
|
|
261 }
|
|
|
262 string validateFileName(string name, bool allowWC, bool preserveExt, charReplace_t replaceIllegalChar) {
|
|
|
263 if (!allowWC) { // special fix for filenames that consist only of question marks
|
|
|
264 size_t end = name.length();
|
|
|
265 if (preserveExt) {
|
|
|
266 size_t offset = pfc::string_find_last(name.c_str(), '.');
|
|
|
267 if (offset < end) end = offset;
|
|
|
268 }
|
|
|
269 bool unnamed = true;
|
|
|
270 for (size_t walk = 0; walk < end; ++walk) {
|
|
|
271 if (name[walk] != '?') unnamed = false;
|
|
|
272 }
|
|
|
273 if (unnamed) {
|
|
|
274 name = string("[unnamed]") + name.subString(end);
|
|
|
275 }
|
|
|
276 }
|
|
|
277
|
|
|
278 // Trailing sanity AFTER replaceIllegalNameChars
|
|
|
279 // replaceIllegalNameChars may remove chars exposing illegal prefix/suffix chars
|
|
|
280 name = replaceIllegalNameChars(name, allowWC, replaceIllegalChar);
|
|
|
281 if (name.length() > 0 && !allowWC) {
|
|
|
282 const char* lstIllegal = preserveExt ? "" : " .";
|
|
|
283 name = trailingSanity(name, preserveExt, lstIllegal);
|
|
|
284 }
|
|
|
285
|
|
|
286 name = truncatePathComponent(name, preserveExt);
|
|
|
287
|
|
|
288 #ifdef _WINDOWS
|
|
|
289 for( auto p : specialIllegalNames ) {
|
|
|
290 if (pfc::stringEqualsI_ascii( name.c_str(), p ) ) {
|
|
|
291 name += "-";
|
|
|
292 break;
|
|
|
293 }
|
|
|
294 }
|
|
|
295 #endif
|
|
|
296
|
|
|
297 if (name.isEmpty()) name = "_";
|
|
|
298 return name;
|
|
|
299 }
|
|
|
300
|
|
|
301 }}} // namespaces
|