1 | // sass.hpp must go before all system headers to get the |
2 | // __EXTENSIONS__ fix on Solaris. |
3 | #include "sass.hpp" |
4 | |
5 | #ifdef _WIN32 |
6 | # ifdef __MINGW32__ |
7 | # ifndef off64_t |
8 | # define off64_t _off64_t /* Workaround for http://sourceforge.net/p/mingw/bugs/2024/ */ |
9 | # endif |
10 | # endif |
11 | # include <direct.h> |
12 | # define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) |
13 | #else |
14 | # include <unistd.h> |
15 | #endif |
16 | #include <cstdio> |
17 | #include <vector> |
18 | #include <algorithm> |
19 | #include <sys/stat.h> |
20 | #include "file.hpp" |
21 | #include "context.hpp" |
22 | #include "prelexer.hpp" |
23 | #include "utf8_string.hpp" |
24 | #include "sass_functions.hpp" |
25 | #include "error_handling.hpp" |
26 | #include "util.hpp" |
27 | #include "util_string.hpp" |
28 | #include "sass2scss.h" |
29 | |
30 | #ifdef _WIN32 |
31 | # include <windows.h> |
32 | |
33 | # ifdef _MSC_VER |
34 | # include <codecvt> |
35 | inline static Sass::sass::string wstring_to_string(const std::wstring& wstr) |
36 | { |
37 | std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> wchar_converter; |
38 | return wchar_converter.to_bytes(wstr); |
39 | } |
40 | # else // mingw(/gcc) does not support C++11's codecvt yet. |
41 | inline static Sass::sass::string wstring_to_string(const std::wstring &wstr) |
42 | { |
43 | int size_needed = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL); |
44 | Sass::sass::string strTo(size_needed, 0); |
45 | WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &strTo[0], size_needed, NULL, NULL); |
46 | return strTo; |
47 | } |
48 | # endif |
49 | #endif |
50 | |
51 | namespace Sass { |
52 | namespace File { |
53 | |
54 | // return the current directory |
55 | // always with forward slashes |
56 | // always with trailing slash |
57 | sass::string get_cwd() |
58 | { |
59 | const size_t wd_len = 4096; |
60 | #ifndef _WIN32 |
61 | char wd[wd_len]; |
62 | char* pwd = getcwd(buf: wd, size: wd_len); |
63 | // we should check error for more detailed info (e.g. ENOENT) |
64 | // http://man7.org/linux/man-pages/man2/getcwd.2.html#ERRORS |
65 | if (pwd == NULL) throw Exception::OperationError("cwd gone missing" ); |
66 | sass::string cwd = pwd; |
67 | #else |
68 | wchar_t wd[wd_len]; |
69 | wchar_t* pwd = _wgetcwd(wd, wd_len); |
70 | if (pwd == NULL) throw Exception::OperationError("cwd gone missing" ); |
71 | sass::string cwd = wstring_to_string(pwd); |
72 | //convert backslashes to forward slashes |
73 | replace(cwd.begin(), cwd.end(), '\\', '/'); |
74 | #endif |
75 | if (cwd[cwd.length() - 1] != '/') cwd += '/'; |
76 | return cwd; |
77 | } |
78 | |
79 | // test if path exists and is a file |
80 | bool file_exists(const sass::string& path) |
81 | { |
82 | #ifdef _WIN32 |
83 | wchar_t resolved[32768]; |
84 | // windows unicode filepaths are encoded in utf16 |
85 | sass::string abspath(join_paths(get_cwd(), path)); |
86 | if (!(abspath[0] == '/' && abspath[1] == '/')) { |
87 | abspath = "//?/" + abspath; |
88 | } |
89 | std::wstring wpath(UTF_8::convert_to_utf16(abspath)); |
90 | std::replace(wpath.begin(), wpath.end(), '/', '\\'); |
91 | DWORD rv = GetFullPathNameW(wpath.c_str(), 32767, resolved, NULL); |
92 | if (rv > 32767) throw Exception::OperationError("Path is too long" ); |
93 | if (rv == 0) throw Exception::OperationError("Path could not be resolved" ); |
94 | DWORD dwAttrib = GetFileAttributesW(resolved); |
95 | return (dwAttrib != INVALID_FILE_ATTRIBUTES && |
96 | (!(dwAttrib & FILE_ATTRIBUTE_DIRECTORY))); |
97 | #else |
98 | struct stat st_buf; |
99 | return (stat (file: path.c_str(), buf: &st_buf) == 0) && |
100 | (!S_ISDIR (st_buf.st_mode)); |
101 | #endif |
102 | } |
103 | |
104 | // return if given path is absolute |
105 | // works with *nix and windows paths |
106 | bool is_absolute_path(const sass::string& path) |
107 | { |
108 | #ifdef _WIN32 |
109 | if (path.length() >= 2 && Util::ascii_isalpha(path[0]) && path[1] == ':') return true; |
110 | #endif |
111 | size_t i = 0; |
112 | // check if we have a protocol |
113 | if (path[i] && Util::ascii_isalpha(c: static_cast<unsigned char>(path[i]))) { |
114 | // skip over all alphanumeric characters |
115 | while (path[i] && Util::ascii_isalnum(c: static_cast<unsigned char>(path[i]))) ++i; |
116 | i = i && path[i] == ':' ? i + 1 : 0; |
117 | } |
118 | return path[i] == '/'; |
119 | } |
120 | |
121 | // helper function to find the last directory separator |
122 | inline size_t find_last_folder_separator(const sass::string& path, size_t limit = sass::string::npos) |
123 | { |
124 | size_t pos; |
125 | size_t pos_p = path.find_last_of(c: '/', pos: limit); |
126 | #ifdef _WIN32 |
127 | size_t pos_w = path.find_last_of('\\', limit); |
128 | #else |
129 | size_t pos_w = sass::string::npos; |
130 | #endif |
131 | if (pos_p != sass::string::npos && pos_w != sass::string::npos) { |
132 | pos = std::max(a: pos_p, b: pos_w); |
133 | } |
134 | else if (pos_p != sass::string::npos) { |
135 | pos = pos_p; |
136 | } |
137 | else { |
138 | pos = pos_w; |
139 | } |
140 | return pos; |
141 | } |
142 | |
143 | // return only the directory part of path |
144 | sass::string dir_name(const sass::string& path) |
145 | { |
146 | size_t pos = find_last_folder_separator(path); |
147 | if (pos == sass::string::npos) return "" ; |
148 | else return path.substr(pos: 0, n: pos+1); |
149 | } |
150 | |
151 | // return only the filename part of path |
152 | sass::string base_name(const sass::string& path) |
153 | { |
154 | size_t pos = find_last_folder_separator(path); |
155 | if (pos == sass::string::npos) return path; |
156 | else return path.substr(pos: pos+1); |
157 | } |
158 | |
159 | // do a logical clean up of the path |
160 | // no physical check on the filesystem |
161 | sass::string make_canonical_path (sass::string path) |
162 | { |
163 | |
164 | // declarations |
165 | size_t pos; |
166 | |
167 | #ifdef _WIN32 |
168 | //convert backslashes to forward slashes |
169 | replace(path.begin(), path.end(), '\\', '/'); |
170 | #endif |
171 | |
172 | pos = 0; // remove all self references inside the path string |
173 | while((pos = path.find(s: "/./" , pos: pos)) != sass::string::npos) path.erase(pos: pos, n: 2); |
174 | |
175 | // remove all leading and trailing self references |
176 | while(path.size() >= 2 && path[0] == '.' && path[1] == '/') path.erase(pos: 0, n: 2); |
177 | while((pos = path.length()) > 1 && path[pos - 2] == '/' && path[pos - 1] == '.') path.erase(pos: pos - 2); |
178 | |
179 | |
180 | size_t proto = 0; |
181 | // check if we have a protocol |
182 | if (path[proto] && Util::ascii_isalpha(c: static_cast<unsigned char>(path[proto]))) { |
183 | // skip over all alphanumeric characters |
184 | while (path[proto] && Util::ascii_isalnum(c: static_cast<unsigned char>(path[proto++]))) {} |
185 | // then skip over the mandatory colon |
186 | if (proto && path[proto] == ':') ++ proto; |
187 | } |
188 | |
189 | // then skip over start slashes |
190 | while (path[proto++] == '/') {} |
191 | |
192 | pos = proto; // collapse multiple delimiters into a single one |
193 | while((pos = path.find(s: "//" , pos: pos)) != sass::string::npos) path.erase(pos: pos, n: 1); |
194 | |
195 | return path; |
196 | |
197 | } |
198 | |
199 | // join two path segments cleanly together |
200 | // but only if right side is not absolute yet |
201 | sass::string join_paths(sass::string l, sass::string r) |
202 | { |
203 | |
204 | #ifdef _WIN32 |
205 | // convert Windows backslashes to URL forward slashes |
206 | replace(l.begin(), l.end(), '\\', '/'); |
207 | replace(r.begin(), r.end(), '\\', '/'); |
208 | #endif |
209 | |
210 | if (l.empty()) return r; |
211 | if (r.empty()) return l; |
212 | |
213 | if (is_absolute_path(path: r)) return r; |
214 | if (l[l.length()-1] != '/') l += '/'; |
215 | |
216 | // this does a logical cleanup of the right hand path |
217 | // Note that this does collapse x/../y sections into y. |
218 | // This is by design. If /foo on your system is a symlink |
219 | // to /bar/baz, then /foo/../cd is actually /bar/cd, |
220 | // not /cd as a naive ../ removal would give you. |
221 | // will only work on leading double dot dirs on rhs |
222 | // therefore it is safe if lhs is already resolved cwd |
223 | while ((r.length() > 3) && ((r.substr(pos: 0, n: 3) == "../" ) || (r.substr(pos: 0, n: 3)) == "..\\" )) { |
224 | size_t L = l.length(), pos = find_last_folder_separator(path: l, limit: L - 2); |
225 | bool is_slash = pos + 2 == L && (l[pos+1] == '/' || l[pos+1] == '\\'); |
226 | bool is_self = pos + 3 == L && (l[pos+1] == '.'); |
227 | if (!is_self && !is_slash) r = r.substr(pos: 3); |
228 | else if (pos == sass::string::npos) break; |
229 | l = l.substr(pos: 0, n: pos == sass::string::npos ? pos : pos + 1); |
230 | } |
231 | |
232 | return l + r; |
233 | } |
234 | |
235 | sass::string path_for_console(const sass::string& rel_path, const sass::string& abs_path, const sass::string& orig_path) |
236 | { |
237 | // magic algorithm goes here!! |
238 | |
239 | // if the file is outside this directory show the absolute path |
240 | if (rel_path.substr(pos: 0, n: 3) == "../" ) { |
241 | return orig_path; |
242 | } |
243 | // this seems to work most of the time |
244 | return abs_path == orig_path ? abs_path : rel_path; |
245 | } |
246 | |
247 | // create an absolute path by resolving relative paths with cwd |
248 | sass::string rel2abs(const sass::string& path, const sass::string& base, const sass::string& cwd) |
249 | { |
250 | sass::string rv = make_canonical_path(path: join_paths(l: join_paths(l: cwd + "/" , r: base + "/" ), r: path)); |
251 | #ifdef _WIN32 |
252 | // On windows we may get an absolute path without directory |
253 | // In that case we should prepend the directory from the root |
254 | if (rv[0] == '/' && rv[1] != '/') { |
255 | rv.insert(0, cwd, 0, 2); |
256 | } |
257 | #endif |
258 | return rv; |
259 | } |
260 | |
261 | // create a path that is relative to the given base directory |
262 | // path and base will first be resolved against cwd to make them absolute |
263 | sass::string abs2rel(const sass::string& path, const sass::string& base, const sass::string& cwd) |
264 | { |
265 | |
266 | sass::string abs_path = rel2abs(path, base: cwd); |
267 | sass::string abs_base = rel2abs(path: base, base: cwd); |
268 | |
269 | size_t proto = 0; |
270 | // check if we have a protocol |
271 | if (path[proto] && Util::ascii_isalpha(c: static_cast<unsigned char>(path[proto]))) { |
272 | // skip over all alphanumeric characters |
273 | while (path[proto] && Util::ascii_isalnum(c: static_cast<unsigned char>(path[proto++]))) {} |
274 | // then skip over the mandatory colon |
275 | if (proto && path[proto] == ':') ++ proto; |
276 | } |
277 | |
278 | // distinguish between windows absolute paths and valid protocols |
279 | // we assume that protocols must at least have two chars to be valid |
280 | if (proto && path[proto++] == '/' && proto > 3) return path; |
281 | |
282 | #ifdef _WIN32 |
283 | // absolute link must have a drive letter, and we know that we |
284 | // can only create relative links if both are on the same drive |
285 | if (abs_base[0] != abs_path[0]) return abs_path; |
286 | #endif |
287 | |
288 | sass::string stripped_uri = "" ; |
289 | sass::string stripped_base = "" ; |
290 | |
291 | size_t index = 0; |
292 | size_t minSize = std::min(a: abs_path.size(), b: abs_base.size()); |
293 | for (size_t i = 0; i < minSize; ++i) { |
294 | #ifdef FS_CASE_SENSITIVE |
295 | if (abs_path[i] != abs_base[i]) break; |
296 | #else |
297 | // compare the charactes in a case insensitive manner |
298 | // windows fs is only case insensitive in ascii ranges |
299 | if (Util::ascii_tolower(static_cast<unsigned char>(abs_path[i])) != |
300 | Util::ascii_tolower(static_cast<unsigned char>(abs_base[i]))) break; |
301 | #endif |
302 | if (abs_path[i] == '/') index = i + 1; |
303 | } |
304 | for (size_t i = index; i < abs_path.size(); ++i) { |
305 | stripped_uri += abs_path[i]; |
306 | } |
307 | for (size_t i = index; i < abs_base.size(); ++i) { |
308 | stripped_base += abs_base[i]; |
309 | } |
310 | |
311 | size_t left = 0; |
312 | size_t directories = 0; |
313 | for (size_t right = 0; right < stripped_base.size(); ++right) { |
314 | if (stripped_base[right] == '/') { |
315 | if (stripped_base.substr(pos: left, n: 2) != ".." ) { |
316 | ++directories; |
317 | } |
318 | else if (directories > 1) { |
319 | --directories; |
320 | } |
321 | else { |
322 | directories = 0; |
323 | } |
324 | left = right + 1; |
325 | } |
326 | } |
327 | |
328 | sass::string result = "" ; |
329 | for (size_t i = 0; i < directories; ++i) { |
330 | result += "../" ; |
331 | } |
332 | result += stripped_uri; |
333 | |
334 | return result; |
335 | } |
336 | |
337 | // Resolution order for ambiguous imports: |
338 | // (1) filename as given |
339 | // (2) underscore + given |
340 | // (3) underscore + given + extension |
341 | // (4) given + extension |
342 | // (5) given + _index.scss |
343 | // (6) given + _index.sass |
344 | sass::vector<Include> resolve_includes(const sass::string& root, const sass::string& file, const sass::vector<sass::string>& exts) |
345 | { |
346 | sass::string filename = join_paths(l: root, r: file); |
347 | // split the filename |
348 | sass::string base(dir_name(path: file)); |
349 | sass::string name(base_name(path: file)); |
350 | sass::vector<Include> includes; |
351 | // create full path (maybe relative) |
352 | sass::string rel_path(join_paths(l: base, r: name)); |
353 | sass::string abs_path(join_paths(l: root, r: rel_path)); |
354 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
355 | // next test variation with underscore |
356 | rel_path = join_paths(l: base, r: "_" + name); |
357 | abs_path = join_paths(l: root, r: rel_path); |
358 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
359 | // next test exts plus underscore |
360 | for(auto ext : exts) { |
361 | rel_path = join_paths(l: base, r: "_" + name + ext); |
362 | abs_path = join_paths(l: root, r: rel_path); |
363 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
364 | } |
365 | // next test plain name with exts |
366 | for(auto ext : exts) { |
367 | rel_path = join_paths(l: base, r: name + ext); |
368 | abs_path = join_paths(l: root, r: rel_path); |
369 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
370 | } |
371 | // index files |
372 | if (includes.size() == 0) { |
373 | // ignore directories that look like @import'able filename |
374 | for(auto ext : exts) { |
375 | if (ends_with(str: name, suffix: ext)) return includes; |
376 | } |
377 | // next test underscore index exts |
378 | for(auto ext : exts) { |
379 | rel_path = join_paths(l: base, r: join_paths(l: name, r: "_index" + ext)); |
380 | abs_path = join_paths(l: root, r: rel_path); |
381 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
382 | } |
383 | // next test plain index exts |
384 | for(auto ext : exts) { |
385 | rel_path = join_paths(l: base, r: join_paths(l: name, r: "index" + ext)); |
386 | abs_path = join_paths(l: root, r: rel_path); |
387 | if (file_exists(path: abs_path)) includes.push_back(x: {{ rel_path, root }, abs_path }); |
388 | } |
389 | } |
390 | // nothing found |
391 | return includes; |
392 | } |
393 | |
394 | sass::vector<sass::string> find_files(const sass::string& file, const sass::vector<sass::string> paths) |
395 | { |
396 | sass::vector<sass::string> includes; |
397 | for (sass::string path : paths) { |
398 | sass::string abs_path(join_paths(l: path, r: file)); |
399 | if (file_exists(path: abs_path)) includes.push_back(x: abs_path); |
400 | } |
401 | return includes; |
402 | } |
403 | |
404 | sass::vector<sass::string> find_files(const sass::string& file, struct Sass_Compiler* compiler) |
405 | { |
406 | // get the last import entry to get current base directory |
407 | // struct Sass_Options* options = sass_compiler_get_options(compiler); |
408 | Sass_Import_Entry import = sass_compiler_get_last_import(compiler); |
409 | const sass::vector<sass::string>& incs = compiler->cpp_ctx->include_paths; |
410 | // create the vector with paths to lookup |
411 | sass::vector<sass::string> paths(1 + incs.size()); |
412 | paths.push_back(x: dir_name(path: import->abs_path)); |
413 | paths.insert(position: paths.end(), first: incs.begin(), last: incs.end()); |
414 | // dispatch to find files in paths |
415 | return find_files(file, paths); |
416 | } |
417 | |
418 | // helper function to search one file in all include paths |
419 | // this is normally not used internally by libsass (C-API sugar) |
420 | sass::string find_file(const sass::string& file, const sass::vector<sass::string> paths) |
421 | { |
422 | if (file.empty()) return file; |
423 | auto res = find_files(file, paths); |
424 | return res.empty() ? "" : res.front(); |
425 | } |
426 | |
427 | // helper function to resolve a filename |
428 | sass::string find_include(const sass::string& file, const sass::vector<sass::string> paths) |
429 | { |
430 | // search in every include path for a match |
431 | for (size_t i = 0, S = paths.size(); i < S; ++i) |
432 | { |
433 | sass::vector<Include> resolved(resolve_includes(root: paths[i], file)); |
434 | if (resolved.size()) return resolved[0].abs_path; |
435 | } |
436 | // nothing found |
437 | return sass::string("" ); |
438 | } |
439 | |
440 | // try to load the given filename |
441 | // returned memory must be freed |
442 | // will auto convert .sass files |
443 | char* read_file(const sass::string& path) |
444 | { |
445 | #ifdef _WIN32 |
446 | BYTE* pBuffer; |
447 | DWORD dwBytes; |
448 | wchar_t resolved[32768]; |
449 | // windows unicode filepaths are encoded in utf16 |
450 | sass::string abspath(join_paths(get_cwd(), path)); |
451 | if (!(abspath[0] == '/' && abspath[1] == '/')) { |
452 | abspath = "//?/" + abspath; |
453 | } |
454 | std::wstring wpath(UTF_8::convert_to_utf16(abspath)); |
455 | std::replace(wpath.begin(), wpath.end(), '/', '\\'); |
456 | DWORD rv = GetFullPathNameW(wpath.c_str(), 32767, resolved, NULL); |
457 | if (rv > 32767) throw Exception::OperationError("Path is too long" ); |
458 | if (rv == 0) throw Exception::OperationError("Path could not be resolved" ); |
459 | HANDLE hFile = CreateFileW(resolved, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); |
460 | if (hFile == INVALID_HANDLE_VALUE) return 0; |
461 | DWORD dwFileLength = GetFileSize(hFile, NULL); |
462 | if (dwFileLength == INVALID_FILE_SIZE) return 0; |
463 | // allocate an extra byte for the null char |
464 | // and another one for edge-cases in lexer |
465 | pBuffer = (BYTE*)malloc((dwFileLength+2)*sizeof(BYTE)); |
466 | ReadFile(hFile, pBuffer, dwFileLength, &dwBytes, NULL); |
467 | pBuffer[dwFileLength+0] = '\0'; |
468 | pBuffer[dwFileLength+1] = '\0'; |
469 | CloseHandle(hFile); |
470 | // just convert from unsigned char* |
471 | char* contents = (char*) pBuffer; |
472 | #else |
473 | // Read the file using `<cstdio>` instead of `<fstream>` for better portability. |
474 | // The `<fstream>` header initializes `<locale>` and this buggy in GCC4/5 with static linking. |
475 | // See: |
476 | // https://www.spinics.net/lists/gcchelp/msg46851.html |
477 | // https://github.com/sass/sassc-ruby/issues/128 |
478 | struct stat st; |
479 | if (stat(file: path.c_str(), buf: &st) == -1 || S_ISDIR(st.st_mode)) return 0; |
480 | FILE* fd = std::fopen(filename: path.c_str(), modes: "rb" ); |
481 | if (fd == nullptr) return nullptr; |
482 | const std::size_t size = st.st_size; |
483 | char* contents = static_cast<char*>(malloc(size: st.st_size + 2 * sizeof(char))); |
484 | if (std::fread(ptr: static_cast<void*>(contents), size: 1, n: size, stream: fd) != size) { |
485 | free(ptr: contents); |
486 | std::fclose(stream: fd); |
487 | return nullptr; |
488 | } |
489 | if (std::fclose(stream: fd) != 0) { |
490 | free(ptr: contents); |
491 | return nullptr; |
492 | } |
493 | contents[size] = '\0'; |
494 | contents[size + 1] = '\0'; |
495 | #endif |
496 | sass::string extension; |
497 | if (path.length() > 5) { |
498 | extension = path.substr(pos: path.length() - 5, n: 5); |
499 | } |
500 | Util::ascii_str_tolower(s: &extension); |
501 | if (extension == ".sass" && contents != 0) { |
502 | char * converted = sass2scss(sass: contents, SASS2SCSS_PRETTIFY_1 | SASS2SCSS_KEEP_COMMENT); |
503 | free(ptr: contents); // free the indented contents |
504 | return converted; // should be freed by caller |
505 | } else { |
506 | return contents; |
507 | } |
508 | } |
509 | |
510 | // split a path string delimited by semicolons or colons (OS dependent) |
511 | sass::vector<sass::string> split_path_list(const char* str) |
512 | { |
513 | sass::vector<sass::string> paths; |
514 | if (str == NULL) return paths; |
515 | // find delimiter via prelexer (return zero at end) |
516 | const char* end = Prelexer::find_first<PATH_SEP>(src: str); |
517 | // search until null delimiter |
518 | while (end) { |
519 | // add path from current position to delimiter |
520 | paths.push_back(x: sass::string(str, end - str)); |
521 | str = end + 1; // skip delimiter |
522 | end = Prelexer::find_first<PATH_SEP>(src: str); |
523 | } |
524 | // add path from current position to end |
525 | paths.push_back(x: sass::string(str)); |
526 | // return back |
527 | return paths; |
528 | } |
529 | |
530 | } |
531 | } |
532 | |