1 /*
2 +----------------------------------------------------------------------+
3 | PHP Version 7 |
4 +----------------------------------------------------------------------+
5 | Copyright (c) 1997-2018 The PHP Group |
6 +----------------------------------------------------------------------+
7 | This source file is subject to version 3.01 of the PHP license, |
8 | that is bundled with this package in the file LICENSE, and is |
9 | available through the world-wide-web at the following url: |
10 | http://www.php.net/license/3_01.txt |
11 | If you did not receive a copy of the PHP license and are unable to |
12 | obtain it through the world-wide-web, please send a note to |
13 | license@php.net so we can mail you a copy immediately. |
14 +----------------------------------------------------------------------+
15 | Author: Anatol Belski <ab@php.net> |
16 +----------------------------------------------------------------------+
17 */
18
19 /* This file integrates several modified parts from the libuv project, which
20 * is copyrighted to
21 *
22 * Copyright Joyent, Inc. and other Node contributors. All rights reserved.
23 *
24 * Permission is hereby granted, free of charge, to any person obtaining a copy
25 * of this software and associated documentation files (the "Software"), to
26 * deal in the Software without restriction, including without limitation the
27 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
28 * sell copies of the Software, and to permit persons to whom the Software is
29 * furnished to do so, subject to the following conditions:
30 *
31 * The above copyright notice and this permission notice shall be included in
32 * all copies or substantial portions of the Software.
33 *
34 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
35 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
36 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
37 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
39 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
40 * IN THE SOFTWARE.
41 */
42
43 #include <assert.h>
44 #include <stdlib.h>
45 #include <direct.h>
46 #include <errno.h>
47 #include <fcntl.h>
48 #include <io.h>
49 #include <limits.h>
50 #include <sys/stat.h>
51 #include <sys/utime.h>
52 #include <stdio.h>
53
54 #include "php.h"
55 #include "SAPI.h"
56 #include "win32/winutil.h"
57 #include "win32/time.h"
58 #include "win32/ioutil.h"
59 #include "win32/codepage.h"
60
61 #include <pathcch.h>
62
63 /*
64 #undef NONLS
65 #undef _WINNLS_
66 #include <winnls.h>
67 */
68
69 typedef HRESULT (__stdcall *MyPathCchCanonicalizeEx)(wchar_t *pszPathOut, size_t cchPathOut, const wchar_t *pszPathIn, unsigned long dwFlags);
70
71 static MyPathCchCanonicalizeEx canonicalize_path_w = NULL;
72
php_win32_ioutil_posix_to_open_opts(int flags,mode_t mode,php_ioutil_open_opts * opts)73 PW32IO BOOL php_win32_ioutil_posix_to_open_opts(int flags, mode_t mode, php_ioutil_open_opts *opts)
74 {/*{{{*/
75 int current_umask;
76
77 opts->attributes = 0;
78
79 /* Obtain the active umask. umask() never fails and returns the previous */
80 /* umask. */
81 current_umask = umask(0);
82 umask(current_umask);
83
84 /* convert flags and mode to CreateFile parameters */
85 switch (flags & (_O_RDONLY | _O_WRONLY | _O_RDWR)) {
86 case _O_RDONLY:
87 opts->access = FILE_GENERIC_READ;
88 /* XXX not opening dirs yet, see also at the bottom of this function. Should be evaluated properly. */
89 /*opts->attributes |= FILE_FLAG_BACKUP_SEMANTICS;*/
90 break;
91 case _O_WRONLY:
92 opts->access = FILE_GENERIC_WRITE;
93 break;
94 case _O_RDWR:
95 opts->access = FILE_GENERIC_READ | FILE_GENERIC_WRITE;
96 break;
97 default:
98 goto einval;
99 }
100
101 if (flags & _O_APPEND) {
102 /* XXX this might look wrong, but i just leave it here. Disabling FILE_WRITE_DATA prevents the current truncate behaviors for files opened with "a". */
103 /* access &= ~FILE_WRITE_DATA;*/
104 opts->access |= FILE_APPEND_DATA;
105 opts->attributes &= ~FILE_FLAG_BACKUP_SEMANTICS;
106 }
107
108 /*
109 * Here is where we deviate significantly from what CRT's _open()
110 * does. We indiscriminately use all the sharing modes, to match
111 * UNIX semantics. In particular, this ensures that the file can
112 * be deleted even whilst it's open.
113 */
114 /* opts->share = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; */
115 /* XXX No UINX behavior Good to know it's doable.
116 Not being done as this means a behavior change. Should be evaluated properly. */
117 opts->share = FILE_SHARE_READ | FILE_SHARE_WRITE;
118
119 switch (flags & (_O_CREAT | _O_EXCL | _O_TRUNC)) {
120 case 0:
121 case _O_EXCL:
122 opts->disposition = OPEN_EXISTING;
123 break;
124 case _O_CREAT:
125 opts->disposition = OPEN_ALWAYS;
126 break;
127 case _O_CREAT | _O_EXCL:
128 case _O_CREAT | _O_TRUNC | _O_EXCL:
129 opts->disposition = CREATE_NEW;
130 break;
131 case _O_TRUNC:
132 case _O_TRUNC | _O_EXCL:
133 opts->disposition = TRUNCATE_EXISTING;
134 break;
135 case _O_CREAT | _O_TRUNC:
136 opts->disposition = CREATE_ALWAYS;
137 break;
138 default:
139 goto einval;
140 }
141
142 opts->attributes |= FILE_ATTRIBUTE_NORMAL;
143 if (flags & _O_CREAT) {
144 if (!((mode & ~current_umask) & _S_IWRITE)) {
145 opts->attributes |= FILE_ATTRIBUTE_READONLY;
146 }
147 }
148
149 if (flags & _O_TEMPORARY ) {
150 opts->attributes |= FILE_FLAG_DELETE_ON_CLOSE | FILE_ATTRIBUTE_TEMPORARY;
151 opts->access |= DELETE;
152 }
153
154 if (flags & _O_SHORT_LIVED) {
155 opts->attributes |= FILE_ATTRIBUTE_TEMPORARY;
156 }
157
158 switch (flags & (_O_SEQUENTIAL | _O_RANDOM)) {
159 case 0:
160 break;
161 case _O_SEQUENTIAL:
162 opts->attributes |= FILE_FLAG_SEQUENTIAL_SCAN;
163 break;
164 case _O_RANDOM:
165 opts->attributes |= FILE_FLAG_RANDOM_ACCESS;
166 break;
167 default:
168 goto einval;
169 }
170
171 /* Very compat options */
172 /*if (flags & O_ASYNC) {
173 opts->attributes |= FILE_FLAG_OVERLAPPED;
174 } else if (flags & O_SYNC) {
175 opts->attributes &= ~FILE_FLAG_OVERLAPPED;
176 }*/
177
178 /* Setting this flag makes it possible to open a directory. */
179 /* XXX not being done as this means a behavior change. Should be evaluated properly. */
180 /* opts->attributes |= FILE_FLAG_BACKUP_SEMANTICS; */
181
182 return 1;
183
184 einval:
185 _set_errno(EINVAL);
186 return 0;
187 }/*}}}*/
188
php_win32_ioutil_open_w(const wchar_t * path,int flags,...)189 PW32IO int php_win32_ioutil_open_w(const wchar_t *path, int flags, ...)
190 {/*{{{*/
191 php_ioutil_open_opts open_opts;
192 HANDLE file;
193 int fd;
194 mode_t mode = 0;
195
196 PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0)
197
198 if (flags & O_CREAT) {
199 va_list arg;
200
201 va_start(arg, flags);
202 mode = (mode_t) va_arg(arg, int);
203 va_end(arg);
204 }
205
206 if (!php_win32_ioutil_posix_to_open_opts(flags, mode, &open_opts)) {
207 goto einval;
208 }
209
210 /* XXX care about security attributes here if needed, see tsrm_win32_access() */
211 file = CreateFileW(path,
212 open_opts.access,
213 open_opts.share,
214 NULL,
215 open_opts.disposition,
216 open_opts.attributes,
217 NULL);
218
219 if (file == INVALID_HANDLE_VALUE) {
220 DWORD error = GetLastError();
221
222 if (error == ERROR_FILE_EXISTS && (flags & _O_CREAT) &&
223 !(flags & _O_EXCL)) {
224 /* Special case: when ERROR_FILE_EXISTS happens and O_CREAT was */
225 /* specified, it means the path referred to a directory. */
226 _set_errno(EISDIR);
227 } else {
228 SET_ERRNO_FROM_WIN32_CODE(error);
229 }
230 return -1;
231 }
232
233 fd = _open_osfhandle((intptr_t) file, flags);
234 if (fd < 0) {
235 DWORD error = GetLastError();
236
237 /* The only known failure mode for _open_osfhandle() is EMFILE, in which
238 * case GetLastError() will return zero. However we'll try to handle other
239 * errors as well, should they ever occur.
240 */
241 if (errno == EMFILE) {
242 _set_errno(EMFILE);
243 } else if (error != ERROR_SUCCESS) {
244 SET_ERRNO_FROM_WIN32_CODE(error);
245 }
246 CloseHandle(file);
247 return -1;
248 }
249
250 if (flags & _O_TEXT) {
251 _setmode(fd, _O_TEXT);
252 } else if (flags & _O_BINARY) {
253 _setmode(fd, _O_BINARY);
254 }
255
256 return fd;
257
258 einval:
259 _set_errno(EINVAL);
260 return -1;
261 }/*}}}*/
262
php_win32_ioutil_close(int fd)263 PW32IO int php_win32_ioutil_close(int fd)
264 {/*{{{*/
265 int result = -1;
266
267 if (-1 == fd) {
268 _set_errno(EBADF);
269 return result;
270 }
271
272 if (fd > 2) {
273 result = _close(fd);
274 } else {
275 result = 0;
276 }
277
278 /* _close doesn't set _doserrno on failure, but it does always set errno
279 * to EBADF on failure.
280 */
281 if (result == -1) {
282 _set_errno(EBADF);
283 }
284
285 return result;
286 }/*}}}*/
287
288 #if 0
289 PW32IO int php_win32_ioutil_mkdir_w(const wchar_t *path, mode_t mode)
290 {/*{{{*/
291 int ret = 0;
292 DWORD err = 0;
293
294 PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0)
295
296 /* TODO extend with mode usage */
297 if (!CreateDirectoryW(path, NULL)) {
298 err = GetLastError();
299 ret = -1;
300 SET_ERRNO_FROM_WIN32_CODE(err);
301 }
302
303 return ret;
304 }/*}}}*/
305 #endif
306
php_win32_ioutil_mkdir(const char * path,mode_t mode)307 PW32IO int php_win32_ioutil_mkdir(const char *path, mode_t mode)
308 {/*{{{*/
309 size_t pathw_len = 0;
310 wchar_t *pathw = php_win32_ioutil_conv_any_to_w(path, 0, &pathw_len);
311 int ret = 0;
312 DWORD err = 0;
313
314 if (pathw_len < _MAX_PATH && pathw_len >= _MAX_PATH - 12) {
315 /* Special case here. From the doc:
316
317 "When using an API to create a directory, the specified path cannot be
318 so long that you cannot append an 8.3 file name ..."
319
320 Thus, if the directory name length happens to be in this range, it
321 already needs to be a long path. The given path is already normalized
322 and prepared, need only to prefix it.
323 */
324 wchar_t *tmp = (wchar_t *) malloc((pathw_len + PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW + 1) * sizeof(wchar_t));
325 if (!tmp) {
326 free(pathw);
327 SET_ERRNO_FROM_WIN32_CODE(ERROR_NOT_ENOUGH_MEMORY);
328 return -1;
329 }
330
331 memmove(tmp, PHP_WIN32_IOUTIL_LONG_PATH_PREFIXW, PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW * sizeof(wchar_t));
332 memmove(tmp+PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW, pathw, pathw_len * sizeof(wchar_t));
333 pathw_len += PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW;
334 tmp[pathw_len] = L'\0';
335
336 free(pathw);
337 pathw = tmp;
338 }
339
340 /* TODO extend with mode usage */
341 if (!pathw) {
342 SET_ERRNO_FROM_WIN32_CODE(ERROR_INVALID_PARAMETER);
343 return -1;
344 }
345
346 PHP_WIN32_IOUTIL_CHECK_PATH_W(pathw, -1, 1)
347
348 if (!CreateDirectoryW(pathw, NULL)) {
349 err = GetLastError();
350 ret = -1;
351 }
352 free(pathw);
353
354 if (0 > ret) {
355 SET_ERRNO_FROM_WIN32_CODE(err);
356 }
357
358 return ret;
359 }/*}}}*/
360
php_win32_ioutil_unlink_w(const wchar_t * path)361 PW32IO int php_win32_ioutil_unlink_w(const wchar_t *path)
362 {/*{{{*/
363 int ret = 0;
364 DWORD err = 0;
365
366 PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0)
367
368 if (!DeleteFileW(path)) {
369 err = GetLastError();
370 ret = -1;
371 SET_ERRNO_FROM_WIN32_CODE(err);
372 }
373
374 return ret;
375 }/*}}}*/
376
php_win32_ioutil_rmdir_w(const wchar_t * path)377 PW32IO int php_win32_ioutil_rmdir_w(const wchar_t *path)
378 {/*{{{*/
379 int ret = 0;
380 DWORD err = 0;
381
382 PHP_WIN32_IOUTIL_CHECK_PATH_W(path, -1, 0)
383
384 if (!RemoveDirectoryW(path)) {
385 err = GetLastError();
386 ret = -1;
387 SET_ERRNO_FROM_WIN32_CODE(err);
388 }
389
390 return ret;
391 }/*}}}*/
392
php_win32_ioutil_chdir_w(const wchar_t * path)393 PW32IO int php_win32_ioutil_chdir_w(const wchar_t *path)
394 {/*{{{*/
395 int ret = 0;
396 DWORD err = 0;
397
398 if (!SetCurrentDirectoryW(path)) {
399 err = GetLastError();
400 ret = -1;
401 SET_ERRNO_FROM_WIN32_CODE(err);
402 }
403
404 return ret;
405 }/*}}}*/
406
php_win32_ioutil_rename_w(const wchar_t * oldname,const wchar_t * newname)407 PW32IO int php_win32_ioutil_rename_w(const wchar_t *oldname, const wchar_t *newname)
408 {/*{{{*/
409 int ret = 0;
410 DWORD err = 0;
411
412 PHP_WIN32_IOUTIL_CHECK_PATH_W(oldname, -1, 0)
413 PHP_WIN32_IOUTIL_CHECK_PATH_W(newname, -1, 0)
414
415
416 if (!MoveFileExW(oldname, newname, MOVEFILE_REPLACE_EXISTING|MOVEFILE_COPY_ALLOWED)) {
417 err = GetLastError();
418 ret = -1;
419 SET_ERRNO_FROM_WIN32_CODE(err);
420 }
421
422 return ret;
423 }/*}}}*/
424
php_win32_ioutil_getcwd_w(const wchar_t * buf,int len)425 PW32IO wchar_t *php_win32_ioutil_getcwd_w(const wchar_t *buf, int len)
426 {/*{{{*/
427 DWORD err = 0;
428 wchar_t *tmp_buf = NULL;
429
430 /* If buf was NULL, the result has to be freed outside here. */
431 if (!buf) {
432 DWORD tmp_len = GetCurrentDirectoryW(0, NULL) + 1;
433 if (!tmp_len) {
434 err = GetLastError();
435 SET_ERRNO_FROM_WIN32_CODE(err);
436 return NULL;
437 } else if (tmp_len > len) {
438 SET_ERRNO_FROM_WIN32_CODE(ERROR_INSUFFICIENT_BUFFER);
439 return NULL;
440 }
441
442 len = tmp_len;
443
444 tmp_buf = (wchar_t *)malloc((len)*sizeof(wchar_t));
445 if (!tmp_buf) {
446 SET_ERRNO_FROM_WIN32_CODE(ERROR_NOT_ENOUGH_MEMORY);
447 return NULL;
448 }
449 buf = tmp_buf;
450 }
451
452 if (!GetCurrentDirectoryW(len, buf)) {
453 err = GetLastError();
454 SET_ERRNO_FROM_WIN32_CODE(err);
455 free(tmp_buf);
456 return NULL;
457 }
458
459 return (wchar_t *)buf;
460 }/*}}}*/
461
462 /* based on zend_dirname(). */
php_win32_ioutil_dirname(char * path,size_t len)463 PW32IO size_t php_win32_ioutil_dirname(char *path, size_t len)
464 {/*{{{*/
465 char *ret = NULL, *start;
466 size_t ret_len, len_adjust = 0, pathw_len;
467 wchar_t *endw, *pathw, *startw;
468
469 if (len == 0) {
470 return 0;
471 }
472
473 start = path;
474
475 /* Don't really care about the path normalization, pure parsing here. */
476 startw = pathw = php_win32_cp_conv_any_to_w(path, len, &pathw_len);
477 if (!pathw) {
478 return 0;
479 }
480
481 endw = pathw + pathw_len - 1;
482
483 if ((2 <= len) && isalpha((int)((unsigned char *)path)[0]) && (':' == path[1])) {
484 pathw += 2;
485 path += 2;
486 len_adjust += 2;
487 if (2 == len) {
488 free(startw);
489 return len;
490 }
491 }
492
493 /* Strip trailing slashes */
494 while (endw >= pathw && PHP_WIN32_IOUTIL_IS_SLASHW(*endw)) {
495 endw--;
496 }
497 if (endw < pathw) {
498 free(startw);
499 /* The path only contained slashes */
500 path[0] = PHP_WIN32_IOUTIL_DEFAULT_SLASH;
501 path[1] = '\0';
502 return 1 + len_adjust;
503 }
504
505 /* Strip filename */
506 while (endw >= pathw && !PHP_WIN32_IOUTIL_IS_SLASHW(*endw)) {
507 endw--;
508 }
509 if (endw < pathw) {
510 free(startw);
511 path[0] = '.';
512 path[1] = '\0';
513 return 1 + len_adjust;
514 }
515
516 /* Strip slashes which came before the file name */
517 while (endw >= pathw && PHP_WIN32_IOUTIL_IS_SLASHW(*endw)) {
518 endw--;
519 }
520 if (endw < pathw) {
521 free(startw);
522 path[0] = PHP_WIN32_IOUTIL_DEFAULT_SLASH;
523 path[1] = '\0';
524 return 1 + len_adjust;
525 }
526 *(endw+1) = L'\0';
527
528 ret_len = (endw + 1 - startw);
529 if (PHP_WIN32_IOUTIL_IS_LONG_PATHW(startw, ret_len)) {
530 ret = php_win32_ioutil_conv_w_to_any(startw + PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW, ret_len - PHP_WIN32_IOUTIL_LONG_PATH_PREFIX_LENW, &ret_len);
531 } else {
532 ret = php_win32_ioutil_conv_w_to_any(startw, ret_len, &ret_len);
533 }
534 memmove(start, ret, ret_len+1);
535 assert(start[ret_len] == '\0');
536 free(ret);
537 free(startw);
538
539 return ret_len;
540 }/*}}}*/
541
542 /* Partial normalization can still be acceptable, explicit fail has to be caught. */
php_win32_ioutil_normalize_path_w(wchar_t ** buf,size_t len,size_t * new_len)543 PW32IO php_win32_ioutil_normalization_result php_win32_ioutil_normalize_path_w(wchar_t **buf, size_t len, size_t *new_len)
544 {/*{{{*/
545 wchar_t *pos, *idx = *buf, canonicalw[MAXPATHLEN];
546 size_t ret_len = len;
547
548 if (len >= MAXPATHLEN) {
549 SET_ERRNO_FROM_WIN32_CODE(ERROR_BAD_LENGTH);
550 *new_len = 0;
551 return PHP_WIN32_IOUTIL_NORM_FAIL;
552 }
553
554 while (NULL != (pos = wcschr(idx, PHP_WIN32_IOUTIL_FW_SLASHW)) && idx - *buf <= len) {
555 *pos = PHP_WIN32_IOUTIL_DEFAULT_SLASHW;
556 idx = pos++;
557 }
558
559 if (S_OK != canonicalize_path_w(canonicalw, MAXPATHLEN, *buf, PATHCCH_ALLOW_LONG_PATHS)) {
560 /* Length unchanged. */
561 *new_len = len;
562 return PHP_WIN32_IOUTIL_NORM_PARTIAL;
563 }
564 ret_len = wcslen(canonicalw);
565 if (ret_len != len) {
566 if (ret_len > len) {
567 wchar_t *tmp = realloc(*buf, (ret_len + 1) * sizeof(wchar_t));
568 if (!tmp) {
569 SET_ERRNO_FROM_WIN32_CODE(ERROR_NOT_ENOUGH_MEMORY);
570 /* Length unchanged. */
571 *new_len = len;
572 return PHP_WIN32_IOUTIL_NORM_PARTIAL;
573 }
574 *buf = tmp;
575 }
576 memmove(*buf, canonicalw, (ret_len + 1) * sizeof(wchar_t));
577 }
578 *new_len = ret_len;
579
580 return PHP_WIN32_IOUTIL_NORM_OK;
581 }/*}}}*/
582
MyPathCchCanonicalizeExFallback(wchar_t * pszPathOut,size_t cchPathOut,const wchar_t * pszPathIn,unsigned long dwFlags)583 static HRESULT __stdcall MyPathCchCanonicalizeExFallback(wchar_t *pszPathOut, size_t cchPathOut, const wchar_t *pszPathIn, unsigned long dwFlags)
584 {/*{{{*/
585 return -42;
586 }/*}}}*/
587
php_win32_ioutil_init(void)588 BOOL php_win32_ioutil_init(void)
589 {/*{{{*/
590 HMODULE hMod = GetModuleHandle("api-ms-win-core-path-l1-1-0");
591
592 if (hMod) {
593 canonicalize_path_w = (MyPathCchCanonicalizeEx)GetProcAddress(hMod, "PathCchCanonicalizeEx");
594 if (!canonicalize_path_w) {
595 canonicalize_path_w = (MyPathCchCanonicalizeEx)MyPathCchCanonicalizeExFallback;
596 }
597 } else {
598 canonicalize_path_w = (MyPathCchCanonicalizeEx)MyPathCchCanonicalizeExFallback;
599 }
600
601 return TRUE;
602 }/*}}}*/
603
604 /* an extended version could be implemented, for now direct functions can be used. */
605 #if 0
606 PW32IO int php_win32_ioutil_access_w(const wchar_t *path, mode_t mode)
607 {
608 return _waccess(path, mode);
609 }
610 #endif
611
612 #if 0
613 PW32IO HANDLE php_win32_ioutil_findfirstfile_w(char *path, WIN32_FIND_DATA *data)
614 {
615 HANDLE ret = INVALID_HANDLE_VALUE;
616 DWORD err;
617
618 if (!path) {
619 SET_ERRNO_FROM_WIN32_CODE(ERROR_INVALID_PARAMETER);
620 return ret;
621 }
622
623 pathw = php_win32_ioutil_any_to_w(path);
624
625 if (!pathw) {
626 err = GetLastError();
627 SET_ERRNO_FROM_WIN32_CODE(ret);
628 return ret;
629 }
630
631 ret = FindFirstFileW(pathw, data);
632
633 if (INVALID_HANDLE_VALUE == ret && path) {
634 ret = FindFirstFileA(path, data);
635 }
636
637 /* XXX set errno */
638 return ret;
639 }
640 #endif
641
642 /*
643 * Local variables:
644 * tab-width: 4
645 * c-basic-offset: 4
646 * End:
647 * vim600: sw=4 ts=4 fdm=marker
648 * vim<600: sw=4 ts=4
649 */
650