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>
35inline 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.
41inline 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
51namespace 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

source code of gtk/subprojects/libsass/src/file.cpp