1 | // This is an open source non-commercial project. Dear PVS-Studio, please check |
2 | // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com |
3 | |
4 | // Environment inspection |
5 | |
6 | #include <assert.h> |
7 | #include <uv.h> |
8 | |
9 | #include "nvim/vim.h" |
10 | #include "nvim/ascii.h" |
11 | #include "nvim/charset.h" |
12 | #include "nvim/fileio.h" |
13 | #include "nvim/os/os.h" |
14 | #include "nvim/memory.h" |
15 | #include "nvim/message.h" |
16 | #include "nvim/path.h" |
17 | #include "nvim/macros.h" |
18 | #include "nvim/strings.h" |
19 | #include "nvim/eval.h" |
20 | #include "nvim/ex_getln.h" |
21 | #include "nvim/version.h" |
22 | #include "nvim/map.h" |
23 | |
24 | #ifdef WIN32 |
25 | #include "nvim/mbyte.h" // for utf8_to_utf16, utf16_to_utf8 |
26 | #endif |
27 | |
28 | #ifdef HAVE__NSGETENVIRON |
29 | #include <crt_externs.h> |
30 | #endif |
31 | |
32 | #ifdef HAVE_SYS_UTSNAME_H |
33 | #include <sys/utsname.h> |
34 | #endif |
35 | |
36 | // Because `uv_os_getenv` requires allocating, we must manage a map to maintain |
37 | // the behavior of `os_getenv`. |
38 | static PMap(cstr_t) *envmap; |
39 | static uv_mutex_t mutex; |
40 | |
41 | void env_init(void) |
42 | { |
43 | envmap = pmap_new(cstr_t)(); |
44 | uv_mutex_init(&mutex); |
45 | } |
46 | |
47 | /// Like getenv(), but returns NULL if the variable is empty. |
48 | /// @see os_env_exists |
49 | const char *os_getenv(const char *name) |
50 | FUNC_ATTR_NONNULL_ALL |
51 | { |
52 | char *e; |
53 | size_t size = 64; |
54 | if (name[0] == '\0') { |
55 | return NULL; |
56 | } |
57 | uv_mutex_lock(&mutex); |
58 | if (pmap_has(cstr_t)(envmap, name) |
59 | && !!(e = (char *)pmap_get(cstr_t)(envmap, name))) { |
60 | if (e[0] != '\0') { |
61 | // Found non-empty cached env var. |
62 | // NOTE: This risks incoherence if an in-process library changes the |
63 | // environment without going through our os_setenv() wrapper. If |
64 | // that turns out to be a problem, we can just remove this codepath. |
65 | goto end; |
66 | } |
67 | pmap_del2(envmap, name); |
68 | } |
69 | e = xmalloc(size); |
70 | int r = uv_os_getenv(name, e, &size); |
71 | if (r == UV_ENOBUFS) { |
72 | e = xrealloc(e, size); |
73 | r = uv_os_getenv(name, e, &size); |
74 | } |
75 | if (r != 0 || size == 0 || e[0] == '\0') { |
76 | xfree(e); |
77 | e = NULL; |
78 | if (r != 0 && r != UV_ENOENT && r != UV_UNKNOWN) { |
79 | ELOG("uv_os_getenv(%s) failed: %d %s" , name, r, uv_err_name(r)); |
80 | } |
81 | goto end; |
82 | } |
83 | pmap_put(cstr_t)(envmap, xstrdup(name), e); |
84 | end: |
85 | uv_mutex_unlock(&mutex); |
86 | return (e == NULL || size == 0 || e[0] == '\0') ? NULL : e; |
87 | } |
88 | |
89 | /// Returns true if environment variable `name` is defined (even if empty). |
90 | /// Returns false if not found (UV_ENOENT) or other failure. |
91 | bool os_env_exists(const char *name) |
92 | FUNC_ATTR_NONNULL_ALL |
93 | { |
94 | if (name[0] == '\0') { |
95 | return false; |
96 | } |
97 | // Use a tiny buffer because we don't care about the value: if uv_os_getenv() |
98 | // returns UV_ENOBUFS, the env var was found. |
99 | char buf[1]; |
100 | size_t size = sizeof(buf); |
101 | int r = uv_os_getenv(name, buf, &size); |
102 | assert(r != UV_EINVAL); |
103 | if (r != 0 && r != UV_ENOENT && r != UV_ENOBUFS) { |
104 | ELOG("uv_os_getenv(%s) failed: %d %s" , name, r, uv_err_name(r)); |
105 | #ifdef WIN32 |
106 | return (r == UV_UNKNOWN); |
107 | #endif |
108 | } |
109 | return (r == 0 || r == UV_ENOBUFS); |
110 | } |
111 | |
112 | /// Sets an environment variable. |
113 | /// |
114 | /// Windows (Vim-compat): Empty string (:let $FOO="") undefines the env var. |
115 | /// |
116 | /// @warning Existing pointers to the result of os_getenv("foo") are |
117 | /// INVALID after os_setenv("foo", …). |
118 | int os_setenv(const char *name, const char *value, int overwrite) |
119 | FUNC_ATTR_NONNULL_ALL |
120 | { |
121 | if (name[0] == '\0') { |
122 | return -1; |
123 | } |
124 | #ifdef WIN32 |
125 | if (!overwrite && os_getenv(name) != NULL) { |
126 | return 0; |
127 | } |
128 | if (value[0] == '\0') { |
129 | // Windows (Vim-compat): Empty string undefines the env var. |
130 | return os_unsetenv(name); |
131 | } |
132 | #else |
133 | if (!overwrite && os_env_exists(name)) { |
134 | return 0; |
135 | } |
136 | #endif |
137 | uv_mutex_lock(&mutex); |
138 | int r = uv_os_setenv(name, value); |
139 | assert(r != UV_EINVAL); |
140 | // Destroy the old map item. Do this AFTER uv_os_setenv(), because `value` |
141 | // could be a previous os_getenv() result. |
142 | pmap_del2(envmap, name); |
143 | if (r != 0) { |
144 | ELOG("uv_os_setenv(%s) failed: %d %s" , name, r, uv_err_name(r)); |
145 | } |
146 | uv_mutex_unlock(&mutex); |
147 | return r == 0 ? 0 : -1; |
148 | } |
149 | |
150 | /// Unset environment variable |
151 | int os_unsetenv(const char *name) |
152 | FUNC_ATTR_NONNULL_ALL |
153 | { |
154 | if (name[0] == '\0') { |
155 | return -1; |
156 | } |
157 | uv_mutex_lock(&mutex); |
158 | pmap_del2(envmap, name); |
159 | int r = uv_os_unsetenv(name); |
160 | if (r != 0) { |
161 | ELOG("uv_os_unsetenv(%s) failed: %d %s" , name, r, uv_err_name(r)); |
162 | } |
163 | uv_mutex_unlock(&mutex); |
164 | return r == 0 ? 0 : -1; |
165 | } |
166 | |
167 | char *os_getenvname_at_index(size_t index) |
168 | { |
169 | #ifdef _WIN32 |
170 | wchar_t *env = GetEnvironmentStringsW(); |
171 | if (!env) { |
172 | return NULL; |
173 | } |
174 | char *name = NULL; |
175 | size_t current_index = 0; |
176 | // GetEnvironmentStringsW() result has this format: |
177 | // var1=value1\0var2=value2\0...varN=valueN\0\0 |
178 | for (wchar_t *it = env; *it != L'\0' || *(it + 1) != L'\0'; it++) { |
179 | if (index == current_index) { |
180 | char *utf8_str; |
181 | int conversion_result = utf16_to_utf8(it, -1, &utf8_str); |
182 | if (conversion_result != 0) { |
183 | EMSG2("utf16_to_utf8 failed: %d" , conversion_result); |
184 | break; |
185 | } |
186 | size_t namesize = 0; |
187 | while (utf8_str[namesize] != '=' && utf8_str[namesize] != NUL) { |
188 | namesize++; |
189 | } |
190 | name = (char *)vim_strnsave((char_u *)utf8_str, namesize); |
191 | xfree(utf8_str); |
192 | break; |
193 | } |
194 | if (*it == L'\0') { |
195 | current_index++; |
196 | } |
197 | } |
198 | |
199 | FreeEnvironmentStringsW(env); |
200 | return name; |
201 | #else |
202 | # if defined(HAVE__NSGETENVIRON) |
203 | char **environ = *_NSGetEnviron(); |
204 | # else |
205 | extern char **environ; |
206 | # endif |
207 | // Check if index is inside the environ array and is not the last element. |
208 | for (size_t i = 0; i <= index; i++) { |
209 | if (environ[i] == NULL) { |
210 | return NULL; |
211 | } |
212 | } |
213 | char *str = environ[index]; |
214 | size_t namesize = 0; |
215 | while (str[namesize] != '=' && str[namesize] != NUL) { |
216 | namesize++; |
217 | } |
218 | char *name = (char *)vim_strnsave((char_u *)str, namesize); |
219 | return name; |
220 | #endif |
221 | } |
222 | |
223 | /// Get the process ID of the Neovim process. |
224 | /// |
225 | /// @return the process ID. |
226 | int64_t os_get_pid(void) |
227 | { |
228 | #ifdef _WIN32 |
229 | return (int64_t)GetCurrentProcessId(); |
230 | #else |
231 | return (int64_t)getpid(); |
232 | #endif |
233 | } |
234 | |
235 | /// Gets the hostname of the current machine. |
236 | /// |
237 | /// @param hostname Buffer to store the hostname. |
238 | /// @param size Size of `hostname`. |
239 | void os_get_hostname(char *hostname, size_t size) |
240 | { |
241 | #ifdef HAVE_SYS_UTSNAME_H |
242 | struct utsname vutsname; |
243 | |
244 | if (uname(&vutsname) < 0) { |
245 | *hostname = '\0'; |
246 | } else { |
247 | xstrlcpy(hostname, vutsname.nodename, size); |
248 | } |
249 | #elif defined(WIN32) |
250 | wchar_t host_utf16[MAX_COMPUTERNAME_LENGTH + 1]; |
251 | DWORD host_wsize = sizeof(host_utf16) / sizeof(host_utf16[0]); |
252 | if (GetComputerNameW(host_utf16, &host_wsize) == 0) { |
253 | *hostname = '\0'; |
254 | DWORD err = GetLastError(); |
255 | EMSG2("GetComputerNameW failed: %d" , err); |
256 | return; |
257 | } |
258 | host_utf16[host_wsize] = '\0'; |
259 | |
260 | char *host_utf8; |
261 | int conversion_result = utf16_to_utf8(host_utf16, -1, &host_utf8); |
262 | if (conversion_result != 0) { |
263 | EMSG2("utf16_to_utf8 failed: %d" , conversion_result); |
264 | return; |
265 | } |
266 | xstrlcpy(hostname, host_utf8, size); |
267 | xfree(host_utf8); |
268 | #else |
269 | EMSG("os_get_hostname failed: missing uname()" ); |
270 | *hostname = '\0'; |
271 | #endif |
272 | } |
273 | |
274 | /// To get the "real" home directory: |
275 | /// - get value of $HOME |
276 | /// For Unix: |
277 | /// - go to that directory |
278 | /// - do os_dirname() to get the real name of that directory. |
279 | /// This also works with mounts and links. |
280 | /// Don't do this for Windows, it will change the "current dir" for a drive. |
281 | static char *homedir = NULL; |
282 | |
283 | void init_homedir(void) |
284 | { |
285 | // In case we are called a second time. |
286 | xfree(homedir); |
287 | homedir = NULL; |
288 | |
289 | const char *var = os_getenv("HOME" ); |
290 | |
291 | #ifdef WIN32 |
292 | // Typically, $HOME is not defined on Windows, unless the user has |
293 | // specifically defined it for Vim's sake. However, on Windows NT |
294 | // platforms, $HOMEDRIVE and $HOMEPATH are automatically defined for |
295 | // each user. Try constructing $HOME from these. |
296 | if (var == NULL) { |
297 | const char *homedrive = os_getenv("HOMEDRIVE" ); |
298 | const char *homepath = os_getenv("HOMEPATH" ); |
299 | if (homepath == NULL) { |
300 | homepath = "\\" ; |
301 | } |
302 | if (homedrive != NULL |
303 | && strlen(homedrive) + strlen(homepath) < MAXPATHL) { |
304 | snprintf(os_buf, MAXPATHL, "%s%s" , homedrive, homepath); |
305 | if (os_buf[0] != NUL) { |
306 | var = os_buf; |
307 | } |
308 | } |
309 | } |
310 | if (var == NULL) { |
311 | var = os_getenv("USERPROFILE" ); |
312 | } |
313 | |
314 | // Weird but true: $HOME may contain an indirect reference to another |
315 | // variable, esp. "%USERPROFILE%". Happens when $USERPROFILE isn't set |
316 | // when $HOME is being set. |
317 | if (var != NULL && *var == '%') { |
318 | const char *p = strchr(var + 1, '%'); |
319 | if (p != NULL) { |
320 | vim_snprintf(os_buf, (size_t)(p - var), "%s" , var + 1); |
321 | const char *exp = os_getenv(os_buf); |
322 | if (exp != NULL && *exp != NUL |
323 | && STRLEN(exp) + STRLEN(p) < MAXPATHL) { |
324 | vim_snprintf(os_buf, MAXPATHL, "%s%s" , exp, p + 1); |
325 | var = os_buf; |
326 | } |
327 | } |
328 | } |
329 | |
330 | // Default home dir is C:/ |
331 | // Best assumption we can make in such a situation. |
332 | if (var == NULL |
333 | // Empty means "undefined" |
334 | || *var == NUL) { |
335 | var = "C:/" ; |
336 | } |
337 | #endif |
338 | |
339 | if (var != NULL) { |
340 | #ifdef UNIX |
341 | // Change to the directory and get the actual path. This resolves |
342 | // links. Don't do it when we can't return. |
343 | if (os_dirname((char_u *)os_buf, MAXPATHL) == OK && os_chdir(os_buf) == 0) { |
344 | if (!os_chdir(var) && os_dirname(IObuff, IOSIZE) == OK) { |
345 | var = (char *)IObuff; |
346 | } |
347 | if (os_chdir(os_buf) != 0) { |
348 | EMSG(_(e_prev_dir)); |
349 | } |
350 | } |
351 | #endif |
352 | homedir = xstrdup(var); |
353 | } |
354 | } |
355 | |
356 | #if defined(EXITFREE) |
357 | |
358 | void free_homedir(void) |
359 | { |
360 | xfree(homedir); |
361 | } |
362 | |
363 | #endif |
364 | |
365 | /// Call expand_env() and store the result in an allocated string. |
366 | /// This is not very memory efficient, this expects the result to be freed |
367 | /// again soon. |
368 | /// @param src String containing environment variables to expand |
369 | /// @see {expand_env} |
370 | char_u *expand_env_save(char_u *src) |
371 | { |
372 | return expand_env_save_opt(src, false); |
373 | } |
374 | |
375 | /// Similar to expand_env_save() but when "one" is `true` handle the string as |
376 | /// one file name, i.e. only expand "~" at the start. |
377 | /// @param src String containing environment variables to expand |
378 | /// @param one Should treat as only one file name |
379 | /// @see {expand_env} |
380 | char_u *expand_env_save_opt(char_u *src, bool one) |
381 | { |
382 | char_u *p = xmalloc(MAXPATHL); |
383 | expand_env_esc(src, p, MAXPATHL, false, one, NULL); |
384 | return p; |
385 | } |
386 | |
387 | /// Expand environment variable with path name. |
388 | /// "~/" is also expanded, using $HOME. For Unix "~user/" is expanded. |
389 | /// Skips over "\ ", "\~" and "\$" (not for Win32 though). |
390 | /// If anything fails no expansion is done and dst equals src. |
391 | /// |
392 | /// @param src Input string e.g. "$HOME/vim.hlp" |
393 | /// @param dst[out] Where to put the result |
394 | /// @param dstlen Maximum length of the result |
395 | void expand_env(char_u *src, char_u *dst, int dstlen) |
396 | { |
397 | expand_env_esc(src, dst, dstlen, false, false, NULL); |
398 | } |
399 | |
400 | /// Expand environment variable with path name and escaping. |
401 | /// @see expand_env |
402 | /// |
403 | /// @param srcp Input string e.g. "$HOME/vim.hlp" |
404 | /// @param dst[out] Where to put the result |
405 | /// @param dstlen Maximum length of the result |
406 | /// @param esc Escape spaces in expanded variables |
407 | /// @param one `srcp` is a single filename |
408 | /// @param prefix Start again after this (can be NULL) |
409 | void expand_env_esc(char_u *restrict srcp, |
410 | char_u *restrict dst, |
411 | int dstlen, |
412 | bool esc, |
413 | bool one, |
414 | char_u *prefix) |
415 | FUNC_ATTR_NONNULL_ARG(1, 2) |
416 | { |
417 | char_u *tail; |
418 | char_u *var; |
419 | bool copy_char; |
420 | bool mustfree; // var was allocated, need to free it later |
421 | bool at_start = true; // at start of a name |
422 | |
423 | int prefix_len = (prefix == NULL) ? 0 : (int)STRLEN(prefix); |
424 | |
425 | char_u *src = skipwhite(srcp); |
426 | dstlen--; // leave one char space for "\," |
427 | while (*src && dstlen > 0) { |
428 | // Skip over `=expr`. |
429 | if (src[0] == '`' && src[1] == '=') { |
430 | var = src; |
431 | src += 2; |
432 | (void)skip_expr(&src); |
433 | if (*src == '`') { |
434 | src++; |
435 | } |
436 | size_t len = (size_t)(src - var); |
437 | if (len > (size_t)dstlen) { |
438 | len = (size_t)dstlen; |
439 | } |
440 | memcpy((char *)dst, (char *)var, len); |
441 | dst += len; |
442 | dstlen -= (int)len; |
443 | continue; |
444 | } |
445 | |
446 | copy_char = true; |
447 | if ((*src == '$') || (*src == '~' && at_start)) { |
448 | mustfree = false; |
449 | |
450 | // The variable name is copied into dst temporarily, because it may |
451 | // be a string in read-only memory and a NUL needs to be appended. |
452 | if (*src != '~') { // environment var |
453 | tail = src + 1; |
454 | var = dst; |
455 | int c = dstlen - 1; |
456 | |
457 | #ifdef UNIX |
458 | // Unix has ${var-name} type environment vars |
459 | if (*tail == '{' && !vim_isIDc('{')) { |
460 | tail++; // ignore '{' |
461 | while (c-- > 0 && *tail != NUL && *tail != '}') { |
462 | *var++ = *tail++; |
463 | } |
464 | } else // NOLINT |
465 | #endif |
466 | { |
467 | while (c-- > 0 && *tail != NUL && vim_isIDc(*tail)) { |
468 | *var++ = *tail++; |
469 | } |
470 | } |
471 | |
472 | #if defined(UNIX) |
473 | // Verify that we have found the end of a Unix ${VAR} style variable |
474 | if (src[1] == '{' && *tail != '}') { |
475 | var = NULL; |
476 | } else { |
477 | if (src[1] == '{') { |
478 | tail++; |
479 | } |
480 | #endif |
481 | *var = NUL; |
482 | var = (char_u *)vim_getenv((char *)dst); |
483 | mustfree = true; |
484 | #if defined(UNIX) |
485 | } |
486 | #endif |
487 | } else if (src[1] == NUL // home directory |
488 | || vim_ispathsep(src[1]) |
489 | || vim_strchr((char_u *)" ,\t\n" , src[1]) != NULL) { |
490 | var = (char_u *)homedir; |
491 | tail = src + 1; |
492 | } else { // user directory |
493 | #if defined(UNIX) |
494 | // Copy ~user to dst[], so we can put a NUL after it. |
495 | tail = src; |
496 | var = dst; |
497 | int c = dstlen - 1; |
498 | while (c-- > 0 |
499 | && *tail |
500 | && vim_isfilec(*tail) |
501 | && !vim_ispathsep(*tail)) { |
502 | *var++ = *tail++; |
503 | } |
504 | *var = NUL; |
505 | // Get the user directory. If this fails the shell is used to expand |
506 | // ~user, which is slower and may fail on old versions of /bin/sh. |
507 | var = (*dst == NUL) ? NULL |
508 | : (char_u *)os_get_user_directory((char *)dst + 1); |
509 | mustfree = true; |
510 | if (var == NULL) { |
511 | expand_T xpc; |
512 | |
513 | ExpandInit(&xpc); |
514 | xpc.xp_context = EXPAND_FILES; |
515 | var = ExpandOne(&xpc, dst, NULL, |
516 | WILD_ADD_SLASH|WILD_SILENT, WILD_EXPAND_FREE); |
517 | mustfree = true; |
518 | } |
519 | #else |
520 | // cannot expand user's home directory, so don't try |
521 | var = NULL; |
522 | tail = (char_u *)"" ; // for gcc |
523 | #endif // UNIX |
524 | } |
525 | |
526 | #ifdef BACKSLASH_IN_FILENAME |
527 | // If 'shellslash' is set change backslashes to forward slashes. |
528 | // Can't use slash_adjust(), p_ssl may be set temporarily. |
529 | if (p_ssl && var != NULL && vim_strchr(var, '\\') != NULL) { |
530 | char_u *p = vim_strsave(var); |
531 | |
532 | if (mustfree) { |
533 | xfree(var); |
534 | } |
535 | var = p; |
536 | mustfree = true; |
537 | forward_slash(var); |
538 | } |
539 | #endif |
540 | |
541 | // If "var" contains white space, escape it with a backslash. |
542 | // Required for ":e ~/tt" when $HOME includes a space. |
543 | if (esc && var != NULL && vim_strpbrk(var, (char_u *)" \t" ) != NULL) { |
544 | char_u *p = vim_strsave_escaped(var, (char_u *)" \t" ); |
545 | |
546 | if (mustfree) { |
547 | xfree(var); |
548 | } |
549 | var = p; |
550 | mustfree = true; |
551 | } |
552 | |
553 | if (var != NULL && *var != NUL |
554 | && (STRLEN(var) + STRLEN(tail) + 1 < (unsigned)dstlen)) { |
555 | STRCPY(dst, var); |
556 | dstlen -= (int)STRLEN(var); |
557 | int c = (int)STRLEN(var); |
558 | // if var[] ends in a path separator and tail[] starts |
559 | // with it, skip a character |
560 | if (*var != NUL && after_pathsep((char *)dst, (char *)dst + c) |
561 | #if defined(BACKSLASH_IN_FILENAME) |
562 | && dst[-1] != ':' |
563 | #endif |
564 | && vim_ispathsep(*tail)) |
565 | ++tail; |
566 | dst += c; |
567 | src = tail; |
568 | copy_char = false; |
569 | } |
570 | if (mustfree) { |
571 | xfree(var); |
572 | } |
573 | } |
574 | |
575 | if (copy_char) { // copy at least one char |
576 | // Recognize the start of a new name, for '~'. |
577 | // Don't do this when "one" is true, to avoid expanding "~" in |
578 | // ":edit foo ~ foo". |
579 | at_start = false; |
580 | if (src[0] == '\\' && src[1] != NUL) { |
581 | *dst++ = *src++; |
582 | --dstlen; |
583 | } else if ((src[0] == ' ' || src[0] == ',') && !one) { |
584 | at_start = true; |
585 | } |
586 | if (dstlen > 0) { |
587 | *dst++ = *src++; |
588 | dstlen--; |
589 | |
590 | if (prefix != NULL |
591 | && src - prefix_len >= srcp |
592 | && STRNCMP(src - prefix_len, prefix, prefix_len) == 0) { |
593 | at_start = true; |
594 | } |
595 | } |
596 | } |
597 | } |
598 | *dst = NUL; |
599 | } |
600 | |
601 | /// Check if the directory "vimdir/<version>" or "vimdir/runtime" exists. |
602 | /// Return NULL if not, return its name in allocated memory otherwise. |
603 | /// @param vimdir directory to test |
604 | static char *vim_version_dir(const char *vimdir) |
605 | { |
606 | if (vimdir == NULL || *vimdir == NUL) { |
607 | return NULL; |
608 | } |
609 | char *p = concat_fnames(vimdir, VIM_VERSION_NODOT, true); |
610 | if (os_isdir((char_u *)p)) { |
611 | return p; |
612 | } |
613 | xfree(p); |
614 | p = concat_fnames(vimdir, RUNTIME_DIRNAME, true); |
615 | if (os_isdir((char_u *)p)) { |
616 | return p; |
617 | } |
618 | xfree(p); |
619 | return NULL; |
620 | } |
621 | |
622 | /// If `dirname + "/"` precedes `pend` in the path, return the pointer to |
623 | /// `dirname + "/" + pend`. Otherwise return `pend`. |
624 | /// |
625 | /// Examples (path = /usr/local/share/nvim/runtime/doc/help.txt): |
626 | /// |
627 | /// pend = help.txt |
628 | /// dirname = doc |
629 | /// -> doc/help.txt |
630 | /// |
631 | /// pend = doc/help.txt |
632 | /// dirname = runtime |
633 | /// -> runtime/doc/help.txt |
634 | /// |
635 | /// pend = runtime/doc/help.txt |
636 | /// dirname = vim74 |
637 | /// -> runtime/doc/help.txt |
638 | /// |
639 | /// @param path Path to a file |
640 | /// @param pend A suffix of the path |
641 | /// @param dirname The immediate path fragment before the pend |
642 | /// @return The new pend including dirname or just pend |
643 | static char *remove_tail(char *path, char *pend, char *dirname) |
644 | { |
645 | size_t len = STRLEN(dirname); |
646 | char *new_tail = pend - len - 1; |
647 | |
648 | if (new_tail >= path |
649 | && fnamencmp((char_u *)new_tail, (char_u *)dirname, len) == 0 |
650 | && (new_tail == path || after_pathsep(path, new_tail))) { |
651 | return new_tail; |
652 | } |
653 | return pend; |
654 | } |
655 | |
656 | /// Iterates $PATH-like delimited list `val`. |
657 | /// |
658 | /// @note Environment variables must not be modified during iteration. |
659 | /// |
660 | /// @param[in] delim Delimiter character. |
661 | /// @param[in] val Value of the environment variable to iterate over. |
662 | /// @param[in] iter Pointer used for iteration. Must be NULL on first |
663 | /// iteration. |
664 | /// @param[out] dir Location where pointer to the start of the current |
665 | /// directory name should be saved. May be set to NULL. |
666 | /// @param[out] len Location where current directory length should be saved. |
667 | /// |
668 | /// @return Next iter argument value or NULL when iteration should stop. |
669 | const void *vim_env_iter(const char delim, |
670 | const char *const val, |
671 | const void *const iter, |
672 | const char **const dir, |
673 | size_t *const len) |
674 | FUNC_ATTR_NONNULL_ARG(2, 4, 5) FUNC_ATTR_WARN_UNUSED_RESULT |
675 | { |
676 | const char *varval = (const char *) iter; |
677 | if (varval == NULL) { |
678 | varval = val; |
679 | } |
680 | *dir = varval; |
681 | const char *const dirend = strchr(varval, delim); |
682 | if (dirend == NULL) { |
683 | *len = strlen(varval); |
684 | return NULL; |
685 | } else { |
686 | *len = (size_t) (dirend - varval); |
687 | return dirend + 1; |
688 | } |
689 | } |
690 | |
691 | /// Iterates $PATH-like delimited list `val` in reverse order. |
692 | /// |
693 | /// @note Environment variables must not be modified during iteration. |
694 | /// |
695 | /// @param[in] delim Delimiter character. |
696 | /// @param[in] val Value of the environment variable to iterate over. |
697 | /// @param[in] iter Pointer used for iteration. Must be NULL on first |
698 | /// iteration. |
699 | /// @param[out] dir Location where pointer to the start of the current |
700 | /// directory name should be saved. May be set to NULL. |
701 | /// @param[out] len Location where current directory length should be saved. |
702 | /// |
703 | /// @return Next iter argument value or NULL when iteration should stop. |
704 | const void *vim_env_iter_rev(const char delim, |
705 | const char *const val, |
706 | const void *const iter, |
707 | const char **const dir, |
708 | size_t *const len) |
709 | FUNC_ATTR_NONNULL_ARG(2, 4, 5) FUNC_ATTR_WARN_UNUSED_RESULT |
710 | { |
711 | const char *varend = (const char *) iter; |
712 | if (varend == NULL) { |
713 | varend = val + strlen(val) - 1; |
714 | } |
715 | const size_t varlen = (size_t)(varend - val) + 1; |
716 | const char *const colon = xmemrchr(val, (uint8_t)delim, varlen); |
717 | if (colon == NULL) { |
718 | *len = varlen; |
719 | *dir = val; |
720 | return NULL; |
721 | } else { |
722 | *dir = colon + 1; |
723 | *len = (size_t) (varend - colon); |
724 | return colon - 1; |
725 | } |
726 | } |
727 | |
728 | /// Vim getenv() wrapper with special handling of $HOME, $VIM, $VIMRUNTIME, |
729 | /// allowing the user to override the Nvim runtime directory at runtime. |
730 | /// Result must be freed by the caller. |
731 | /// |
732 | /// @param name Environment variable to expand |
733 | /// @return [allocated] Expanded environment variable, or NULL |
734 | char *vim_getenv(const char *name) |
735 | { |
736 | // init_path() should have been called before now. |
737 | assert(get_vim_var_str(VV_PROGPATH)[0] != NUL); |
738 | |
739 | #ifdef WIN32 |
740 | if (strcmp(name, "HOME" ) == 0) { |
741 | return xstrdup(homedir); |
742 | } |
743 | #endif |
744 | |
745 | const char *kos_env_path = os_getenv(name); |
746 | if (kos_env_path != NULL) { |
747 | return xstrdup(kos_env_path); |
748 | } |
749 | |
750 | bool vimruntime = (strcmp(name, "VIMRUNTIME" ) == 0); |
751 | if (!vimruntime && strcmp(name, "VIM" ) != 0) { |
752 | return NULL; |
753 | } |
754 | |
755 | // When expanding $VIMRUNTIME fails, try using $VIM/vim<version> or $VIM. |
756 | // Don't do this when default_vimruntime_dir is non-empty. |
757 | char *vim_path = NULL; |
758 | if (vimruntime |
759 | #ifdef HAVE_PATHDEF |
760 | && *default_vimruntime_dir == NUL |
761 | #endif |
762 | ) { |
763 | kos_env_path = os_getenv("VIM" ); |
764 | if (kos_env_path != NULL) { |
765 | vim_path = vim_version_dir(kos_env_path); |
766 | if (vim_path == NULL) { |
767 | vim_path = xstrdup(kos_env_path); |
768 | } |
769 | } |
770 | } |
771 | |
772 | // When expanding $VIM or $VIMRUNTIME fails, try using: |
773 | // - the directory name from 'helpfile' (unless it contains '$') |
774 | // - the executable name from argv[0] |
775 | if (vim_path == NULL) { |
776 | if (p_hf != NULL && vim_strchr(p_hf, '$') == NULL) { |
777 | vim_path = (char *)p_hf; |
778 | } |
779 | |
780 | char exe_name[MAXPATHL]; |
781 | // Find runtime path relative to the nvim binary: ../share/nvim/runtime |
782 | if (vim_path == NULL) { |
783 | xstrlcpy(exe_name, (char *)get_vim_var_str(VV_PROGPATH), |
784 | sizeof(exe_name)); |
785 | char *path_end = (char *)path_tail_with_sep((char_u *)exe_name); |
786 | *path_end = '\0'; // remove the trailing "nvim.exe" |
787 | path_end = (char *)path_tail((char_u *)exe_name); |
788 | *path_end = '\0'; // remove the trailing "bin/" |
789 | if (append_path( |
790 | exe_name, |
791 | "share" _PATHSEPSTR "nvim" _PATHSEPSTR "runtime" _PATHSEPSTR, |
792 | MAXPATHL) == OK) { |
793 | vim_path = exe_name; // -V507 |
794 | } |
795 | } |
796 | |
797 | if (vim_path != NULL) { |
798 | // remove the file name |
799 | char *vim_path_end = (char *)path_tail((char_u *)vim_path); |
800 | |
801 | // remove "doc/" from 'helpfile', if present |
802 | if (vim_path == (char *)p_hf) { |
803 | vim_path_end = remove_tail(vim_path, vim_path_end, "doc" ); |
804 | } |
805 | |
806 | // for $VIM, remove "runtime/" or "vim54/", if present |
807 | if (!vimruntime) { |
808 | vim_path_end = remove_tail(vim_path, vim_path_end, RUNTIME_DIRNAME); |
809 | vim_path_end = remove_tail(vim_path, vim_path_end, VIM_VERSION_NODOT); |
810 | } |
811 | |
812 | // remove trailing path separator |
813 | if (vim_path_end > vim_path && after_pathsep(vim_path, vim_path_end)) { |
814 | vim_path_end--; |
815 | } |
816 | |
817 | // check that the result is a directory name |
818 | assert(vim_path_end >= vim_path); |
819 | vim_path = xstrndup(vim_path, (size_t)(vim_path_end - vim_path)); |
820 | |
821 | if (!os_isdir((char_u *)vim_path)) { |
822 | xfree(vim_path); |
823 | vim_path = NULL; |
824 | } |
825 | } |
826 | assert(vim_path != exe_name); |
827 | } |
828 | |
829 | #ifdef HAVE_PATHDEF |
830 | // When there is a pathdef.c file we can use default_vim_dir and |
831 | // default_vimruntime_dir |
832 | if (vim_path == NULL) { |
833 | // Only use default_vimruntime_dir when it is not empty |
834 | if (vimruntime && *default_vimruntime_dir != NUL) { |
835 | vim_path = xstrdup(default_vimruntime_dir); |
836 | } else if (*default_vim_dir != NUL) { |
837 | if (vimruntime |
838 | && (vim_path = vim_version_dir(default_vim_dir)) == NULL) { |
839 | vim_path = xstrdup(default_vim_dir); |
840 | } |
841 | } |
842 | } |
843 | #endif |
844 | |
845 | // Set the environment variable, so that the new value can be found fast |
846 | // next time, and others can also use it (e.g. Perl). |
847 | if (vim_path != NULL) { |
848 | if (vimruntime) { |
849 | os_setenv("VIMRUNTIME" , vim_path, 1); |
850 | didset_vimruntime = true; |
851 | } else { |
852 | os_setenv("VIM" , vim_path, 1); |
853 | didset_vim = true; |
854 | } |
855 | } |
856 | return vim_path; |
857 | } |
858 | |
859 | /// Replace home directory by "~" in each space or comma separated file name in |
860 | /// 'src'. |
861 | /// |
862 | /// Replace home directory with tilde in each file name |
863 | /// |
864 | /// If anything fails (except when out of space) dst equals src. |
865 | /// |
866 | /// @param[in] buf When not NULL, uses this buffer to check whether it is |
867 | /// a help file. If it is then path to file is removed |
868 | /// completely, `one` is ignored and assumed to be true. |
869 | /// @param[in] src Input file names. Assumed to be a space/comma separated |
870 | /// list unless `one` is true. |
871 | /// @param[out] dst Where to put the result. |
872 | /// @param[in] dstlen Destination length. |
873 | /// @param[in] one If true, assumes source is a single file name and not |
874 | /// a list of them. |
875 | /// |
876 | /// @return length of the string put into dst, does not include NUL byte. |
877 | size_t home_replace(const buf_T *const buf, const char_u *src, |
878 | char_u *const dst, size_t dstlen, const bool one) |
879 | FUNC_ATTR_NONNULL_ARG(3) |
880 | { |
881 | size_t dirlen = 0; |
882 | size_t envlen = 0; |
883 | |
884 | if (src == NULL) { |
885 | *dst = NUL; |
886 | return 0; |
887 | } |
888 | |
889 | if (buf != NULL && buf->b_help) { |
890 | const size_t dlen = xstrlcpy((char *)dst, (char *)path_tail(src), dstlen); |
891 | return MIN(dlen, dstlen - 1); |
892 | } |
893 | |
894 | // We check both the value of the $HOME environment variable and the |
895 | // "real" home directory. |
896 | if (homedir != NULL) { |
897 | dirlen = strlen(homedir); |
898 | } |
899 | |
900 | const char *homedir_env = os_getenv("HOME" ); |
901 | #ifdef WIN32 |
902 | if (homedir_env == NULL) { |
903 | homedir_env = os_getenv("USERPROFILE" ); |
904 | } |
905 | #endif |
906 | char *homedir_env_mod = (char *)homedir_env; |
907 | bool must_free = false; |
908 | |
909 | if (homedir_env_mod != NULL && *homedir_env_mod == '~') { |
910 | must_free = true; |
911 | size_t usedlen = 0; |
912 | size_t flen = strlen(homedir_env_mod); |
913 | char_u *fbuf = NULL; |
914 | (void)modify_fname((char_u *)":p" , false, &usedlen, |
915 | (char_u **)&homedir_env_mod, &fbuf, &flen); |
916 | flen = strlen(homedir_env_mod); |
917 | assert(homedir_env_mod != homedir_env); |
918 | if (vim_ispathsep(homedir_env_mod[flen - 1])) { |
919 | // Remove the trailing / that is added to a directory. |
920 | homedir_env_mod[flen - 1] = NUL; |
921 | } |
922 | } |
923 | |
924 | if (homedir_env_mod != NULL) { |
925 | envlen = strlen(homedir_env_mod); |
926 | } |
927 | |
928 | if (!one) { |
929 | src = skipwhite(src); |
930 | } |
931 | char *dst_p = (char *)dst; |
932 | while (*src && dstlen > 0) { |
933 | // Here we are at the beginning of a file name. |
934 | // First, check to see if the beginning of the file name matches |
935 | // $HOME or the "real" home directory. Check that there is a '/' |
936 | // after the match (so that if e.g. the file is "/home/pieter/bla", |
937 | // and the home directory is "/home/piet", the file does not end up |
938 | // as "~er/bla" (which would seem to indicate the file "bla" in user |
939 | // er's home directory)). |
940 | char *p = homedir; |
941 | size_t len = dirlen; |
942 | for (;;) { |
943 | if (len |
944 | && fnamencmp(src, (char_u *)p, len) == 0 |
945 | && (vim_ispathsep(src[len]) |
946 | || (!one && (src[len] == ',' || src[len] == ' ')) |
947 | || src[len] == NUL)) { |
948 | src += len; |
949 | if (--dstlen > 0) { |
950 | *dst_p++ = '~'; |
951 | } |
952 | |
953 | // If it's just the home directory, add "/". |
954 | if (!vim_ispathsep(src[0]) && --dstlen > 0) { |
955 | *dst_p++ = '/'; |
956 | } |
957 | break; |
958 | } |
959 | if (p == homedir_env_mod) { |
960 | break; |
961 | } |
962 | p = homedir_env_mod; |
963 | len = envlen; |
964 | } |
965 | |
966 | // if (!one) skip to separator: space or comma. |
967 | while (*src && (one || (*src != ',' && *src != ' ')) && --dstlen > 0) { |
968 | *dst_p++ = (char)(*src++); |
969 | } |
970 | // Skip separator. |
971 | while ((*src == ' ' || *src == ',') && --dstlen > 0) { |
972 | *dst_p++ = (char)(*src++); |
973 | } |
974 | } |
975 | // If (dstlen == 0) out of space, what to do??? |
976 | |
977 | *dst_p = NUL; |
978 | |
979 | if (must_free) { |
980 | xfree(homedir_env_mod); |
981 | } |
982 | return (size_t)(dst_p - (char *)dst); |
983 | } |
984 | |
985 | /// Like home_replace, store the replaced string in allocated memory. |
986 | /// @param buf When not NULL, check for help files |
987 | /// @param src Input file name |
988 | char_u * home_replace_save(buf_T *buf, char_u *src) FUNC_ATTR_NONNULL_RET |
989 | { |
990 | size_t len = 3; // space for "~/" and trailing NUL |
991 | if (src != NULL) { // just in case |
992 | len += STRLEN(src); |
993 | } |
994 | char_u *dst = xmalloc(len); |
995 | home_replace(buf, src, dst, len, true); |
996 | return dst; |
997 | } |
998 | |
999 | |
1000 | /// Function given to ExpandGeneric() to obtain an environment variable name. |
1001 | char_u *get_env_name(expand_T *xp, int idx) |
1002 | { |
1003 | # define ENVNAMELEN 100 |
1004 | // this static buffer is needed to avoid a memory leak in ExpandGeneric |
1005 | static char_u name[ENVNAMELEN]; |
1006 | assert(idx >= 0); |
1007 | char *envname = os_getenvname_at_index((size_t)idx); |
1008 | if (envname) { |
1009 | STRLCPY(name, envname, ENVNAMELEN); |
1010 | xfree(envname); |
1011 | return name; |
1012 | } |
1013 | return NULL; |
1014 | } |
1015 | |
1016 | /// Appends the head of `fname` to $PATH and sets it in the environment. |
1017 | /// |
1018 | /// @param fname Full path whose parent directory will be appended to $PATH. |
1019 | /// |
1020 | /// @return true if `path` was appended-to |
1021 | bool os_setenv_append_path(const char *fname) |
1022 | FUNC_ATTR_NONNULL_ALL |
1023 | { |
1024 | #ifdef WIN32 |
1025 | // 8191 (plus NUL) is considered the practical maximum. |
1026 | # define MAX_ENVPATHLEN 8192 |
1027 | #else |
1028 | // No prescribed maximum on unix. |
1029 | # define MAX_ENVPATHLEN INT_MAX |
1030 | #endif |
1031 | if (!path_is_absolute((char_u *)fname)) { |
1032 | internal_error("os_setenv_append_path()" ); |
1033 | return false; |
1034 | } |
1035 | const char *tail = (char *)path_tail_with_sep((char_u *)fname); |
1036 | size_t dirlen = (size_t)(tail - fname); |
1037 | assert(tail >= fname && dirlen + 1 < sizeof(os_buf)); |
1038 | xstrlcpy(os_buf, fname, dirlen + 1); |
1039 | const char *path = os_getenv("PATH" ); |
1040 | const size_t pathlen = path ? strlen(path) : 0; |
1041 | const size_t newlen = pathlen + dirlen + 2; |
1042 | if (newlen < MAX_ENVPATHLEN) { |
1043 | char *temp = xmalloc(newlen); |
1044 | if (pathlen == 0) { |
1045 | temp[0] = NUL; |
1046 | } else { |
1047 | xstrlcpy(temp, path, newlen); |
1048 | xstrlcat(temp, ENV_SEPSTR, newlen); |
1049 | } |
1050 | xstrlcat(temp, os_buf, newlen); |
1051 | os_setenv("PATH" , temp, 1); |
1052 | xfree(temp); |
1053 | return true; |
1054 | } |
1055 | return false; |
1056 | } |
1057 | |
1058 | /// Returns true if `sh` looks like it resolves to "cmd.exe". |
1059 | bool os_shell_is_cmdexe(const char *sh) |
1060 | FUNC_ATTR_NONNULL_ALL |
1061 | { |
1062 | if (*sh == NUL) { |
1063 | return false; |
1064 | } |
1065 | if (striequal(sh, "$COMSPEC" )) { |
1066 | const char *comspec = os_getenv("COMSPEC" ); |
1067 | return striequal("cmd.exe" , (char *)path_tail((char_u *)comspec)); |
1068 | } |
1069 | if (striequal(sh, "cmd.exe" ) || striequal(sh, "cmd" )) { |
1070 | return true; |
1071 | } |
1072 | return striequal("cmd.exe" , (char *)path_tail((char_u *)sh)); |
1073 | } |
1074 | |