blob: 6144d810fb5b6edea385c987eca530eb1ff6a767 [file] [log] [blame]
/* Copyright (C) 1999-2019 by The D Language Foundation, All Rights Reserved
* http://www.digitalmars.com
* Distributed under the Boost Software License, Version 1.0.
* (See accompanying file LICENSE or copy at http://www.boost.org/LICENSE_1_0.txt)
* https://github.com/D-Programming-Language/dmd/blob/master/src/root/filename.c
*/
#include "dsystem.h"
#include "filename.h"
#include "outbuffer.h"
#include "array.h"
#include "file.h"
#include "rmem.h"
#if _WIN32
#include <windows.h>
#endif
#if POSIX
#include <utime.h>
#endif
/****************************** FileName ********************************/
FileName::FileName(const char *str)
: str(mem.xstrdup(str))
{
}
const char *FileName::combine(const char *path, const char *name)
{ char *f;
size_t pathlen;
size_t namelen;
if (!path || !*path)
return name;
pathlen = strlen(path);
namelen = strlen(name);
f = (char *)mem.xmalloc(pathlen + 1 + namelen + 1);
memcpy(f, path, pathlen);
#if POSIX
if (path[pathlen - 1] != '/')
{ f[pathlen] = '/';
pathlen++;
}
#elif _WIN32
if (path[pathlen - 1] != '\\' &&
path[pathlen - 1] != '/' &&
path[pathlen - 1] != ':')
{ f[pathlen] = '\\';
pathlen++;
}
#else
assert(0);
#endif
memcpy(f + pathlen, name, namelen + 1);
return f;
}
// Split a path into an Array of paths
Strings *FileName::splitPath(const char *path)
{
char c = 0; // unnecessary initializer is for VC /W4
const char *p;
OutBuffer buf;
Strings *array;
array = new Strings();
if (path)
{
p = path;
do
{ char instring = 0;
while (isspace((utf8_t)*p)) // skip leading whitespace
p++;
buf.reserve(strlen(p) + 1); // guess size of path
for (; ; p++)
{
c = *p;
switch (c)
{
case '"':
instring ^= 1; // toggle inside/outside of string
continue;
#if MACINTOSH
case ',':
#endif
#if _WIN32
case ';':
#endif
#if POSIX
case ':':
#endif
p++;
break; // note that ; cannot appear as part
// of a path, quotes won't protect it
case 0x1A: // ^Z means end of file
case 0:
break;
case '\r':
continue; // ignore carriage returns
#if POSIX
case '~':
{
char *home = getenv("HOME");
if (home)
buf.writestring(home);
else
buf.writestring("~");
continue;
}
#endif
default:
buf.writeByte(c);
continue;
}
break;
}
if (buf.offset) // if path is not empty
{
array->push(buf.extractString());
}
} while (c);
}
return array;
}
int FileName::compare(RootObject *obj)
{
return compare(str, ((FileName *)obj)->str);
}
int FileName::compare(const char *name1, const char *name2)
{
#if _WIN32
return stricmp(name1, name2);
#else
return strcmp(name1, name2);
#endif
}
bool FileName::equals(RootObject *obj)
{
return compare(obj) == 0;
}
bool FileName::equals(const char *name1, const char *name2)
{
return compare(name1, name2) == 0;
}
/************************************
* Return !=0 if absolute path name.
*/
bool FileName::absolute(const char *name)
{
#if _WIN32
return (*name == '\\') ||
(*name == '/') ||
(*name && name[1] == ':');
#elif POSIX
return (*name == '/');
#else
assert(0);
#endif
}
/********************************
* Return filename extension (read-only).
* Points past '.' of extension.
* If there isn't one, return NULL.
*/
const char *FileName::ext(const char *str)
{
size_t len = strlen(str);
const char *e = str + len;
for (;;)
{
switch (*e)
{ case '.':
return e + 1;
#if POSIX
case '/':
break;
#endif
#if _WIN32
case '\\':
case ':':
case '/':
break;
#endif
default:
if (e == str)
break;
e--;
continue;
}
return NULL;
}
}
const char *FileName::ext()
{
return ext(str);
}
/********************************
* Return mem.xmalloc'd filename with extension removed.
*/
const char *FileName::removeExt(const char *str)
{
const char *e = ext(str);
if (e)
{ size_t len = (e - str) - 1;
char *n = (char *)mem.xmalloc(len + 1);
memcpy(n, str, len);
n[len] = 0;
return n;
}
return mem.xstrdup(str);
}
/********************************
* Return filename name excluding path (read-only).
*/
const char *FileName::name(const char *str)
{
size_t len = strlen(str);
const char *e = str + len;
for (;;)
{
switch (*e)
{
#if POSIX
case '/':
return e + 1;
#endif
#if _WIN32
case '/':
case '\\':
return e + 1;
case ':':
/* The ':' is a drive letter only if it is the second
* character or the last character,
* otherwise it is an ADS (Alternate Data Stream) separator.
* Consider ADS separators as part of the file name.
*/
if (e == str + 1 || e == str + len - 1)
return e + 1;
#endif
/* falls through */
default:
if (e == str)
break;
e--;
continue;
}
return e;
}
}
const char *FileName::name()
{
return name(str);
}
/**************************************
* Return path portion of str.
* Path will does not include trailing path separator.
*/
const char *FileName::path(const char *str)
{
const char *n = name(str);
size_t pathlen;
if (n > str)
{
#if POSIX
if (n[-1] == '/')
n--;
#elif _WIN32
if (n[-1] == '\\' || n[-1] == '/')
n--;
#else
assert(0);
#endif
}
pathlen = n - str;
char *path = (char *)mem.xmalloc(pathlen + 1);
memcpy(path, str, pathlen);
path[pathlen] = 0;
return path;
}
/**************************************
* Replace filename portion of path.
*/
const char *FileName::replaceName(const char *path, const char *name)
{
size_t pathlen;
size_t namelen;
if (absolute(name))
return name;
const char *n = FileName::name(path);
if (n == path)
return name;
pathlen = n - path;
namelen = strlen(name);
char *f = (char *)mem.xmalloc(pathlen + 1 + namelen + 1);
memcpy(f, path, pathlen);
#if POSIX
if (path[pathlen - 1] != '/')
{ f[pathlen] = '/';
pathlen++;
}
#elif _WIN32
if (path[pathlen - 1] != '\\' &&
path[pathlen - 1] != '/' &&
path[pathlen - 1] != ':')
{ f[pathlen] = '\\';
pathlen++;
}
#else
assert(0);
#endif
memcpy(f + pathlen, name, namelen + 1);
return f;
}
/***************************
* Free returned value with FileName::free()
*/
const char *FileName::defaultExt(const char *name, const char *ext)
{
const char *e = FileName::ext(name);
if (e) // if already has an extension
return mem.xstrdup(name);
size_t len = strlen(name);
size_t extlen = strlen(ext);
char *s = (char *)mem.xmalloc(len + 1 + extlen + 1);
memcpy(s,name,len);
s[len] = '.';
memcpy(s + len + 1, ext, extlen + 1);
return s;
}
/***************************
* Free returned value with FileName::free()
*/
const char *FileName::forceExt(const char *name, const char *ext)
{
const char *e = FileName::ext(name);
if (e) // if already has an extension
{
size_t len = e - name;
size_t extlen = strlen(ext);
char *s = (char *)mem.xmalloc(len + extlen + 1);
memcpy(s,name,len);
memcpy(s + len, ext, extlen + 1);
return s;
}
else
return defaultExt(name, ext); // doesn't have one
}
/******************************
* Return !=0 if extensions match.
*/
bool FileName::equalsExt(const char *ext)
{
return equalsExt(str, ext);
}
bool FileName::equalsExt(const char *name, const char *ext)
{
const char *e = FileName::ext(name);
if (!e && !ext)
return true;
if (!e || !ext)
return false;
return FileName::compare(e, ext) == 0;
}
/*************************************
* Search Path for file.
* Input:
* cwd if true, search current directory before searching path
*/
const char *FileName::searchPath(Strings *path, const char *name, bool cwd)
{
if (absolute(name))
{
return exists(name) ? name : NULL;
}
if (cwd)
{
if (exists(name))
return name;
}
if (path)
{
for (size_t i = 0; i < path->dim; i++)
{
const char *p = (*path)[i];
const char *n = combine(p, name);
if (exists(n))
return n;
}
}
return NULL;
}
/*************************************
* Search Path for file in a safe manner.
*
* Be wary of CWE-22: Improper Limitation of a Pathname to a Restricted Directory
* ('Path Traversal') attacks.
* http://cwe.mitre.org/data/definitions/22.html
* More info:
* https://www.securecoding.cert.org/confluence/display/c/FIO02-C.+Canonicalize+path+names+originating+from+tainted+sources
* Returns:
* NULL file not found
* !=NULL mem.xmalloc'd file name
*/
const char *FileName::safeSearchPath(Strings *path, const char *name)
{
#if _WIN32
// don't allow leading / because it might be an absolute
// path or UNC path or something we'd prefer to just not deal with
if (*name == '/')
{
return NULL;
}
/* Disallow % \ : and .. in name characters
* We allow / for compatibility with subdirectories which is allowed
* on dmd/posix. With the leading / blocked above and the rest of these
* conservative restrictions, we should be OK.
*/
for (const char *p = name; *p; p++)
{
char c = *p;
if (c == '\\' || c == ':' || c == '%' || (c == '.' && p[1] == '.'))
{
return NULL;
}
}
return FileName::searchPath(path, name, false);
#elif POSIX
/* Even with realpath(), we must check for // and disallow it
*/
for (const char *p = name; *p; p++)
{
char c = *p;
if (c == '/' && p[1] == '/')
{
return NULL;
}
}
if (path)
{
/* Each path is converted to a cannonical name and then a check is done to see
* that the searched name is really a child one of the the paths searched.
*/
for (size_t i = 0; i < path->dim; i++)
{
const char *cname = NULL;
const char *cpath = canonicalName((*path)[i]);
//printf("FileName::safeSearchPath(): name=%s; path=%s; cpath=%s\n",
// name, (char *)path->data[i], cpath);
if (cpath == NULL)
goto cont;
cname = canonicalName(combine(cpath, name));
//printf("FileName::safeSearchPath(): cname=%s\n", cname);
if (cname == NULL)
goto cont;
//printf("FileName::safeSearchPath(): exists=%i "
// "strncmp(cpath, cname, %i)=%i\n", exists(cname),
// strlen(cpath), strncmp(cpath, cname, strlen(cpath)));
// exists and name is *really* a "child" of path
if (exists(cname) && strncmp(cpath, cname, strlen(cpath)) == 0)
{
::free(const_cast<char *>(cpath));
const char *p = mem.xstrdup(cname);
::free(const_cast<char *>(cname));
return p;
}
cont:
if (cpath)
::free(const_cast<char *>(cpath));
if (cname)
::free(const_cast<char *>(cname));
}
}
return NULL;
#else
assert(0);
#endif
}
int FileName::exists(const char *name)
{
#if POSIX
struct stat st;
if (stat(name, &st) < 0)
return 0;
if (S_ISDIR(st.st_mode))
return 2;
return 1;
#elif _WIN32
DWORD dw;
int result;
dw = GetFileAttributesA(name);
if (dw == INVALID_FILE_ATTRIBUTES)
result = 0;
else if (dw & FILE_ATTRIBUTE_DIRECTORY)
result = 2;
else
result = 1;
return result;
#else
assert(0);
#endif
}
bool FileName::ensurePathExists(const char *path)
{
//printf("FileName::ensurePathExists(%s)\n", path ? path : "");
if (path && *path)
{
if (!exists(path))
{
const char *p = FileName::path(path);
if (*p)
{
#if _WIN32
size_t len = strlen(path);
if ((len > 2 && p[-1] == ':' && strcmp(path + 2, p) == 0) ||
len == strlen(p))
{ mem.xfree(const_cast<char *>(p));
return 0;
}
#endif
bool r = ensurePathExists(p);
mem.xfree(const_cast<char *>(p));
if (r)
return r;
}
#if _WIN32
char sep = '\\';
#elif POSIX
char sep = '/';
#endif
if (path[strlen(path) - 1] != sep)
{
//printf("mkdir(%s)\n", path);
#if _WIN32
int r = _mkdir(path);
#endif
#if POSIX
int r = mkdir(path, (7 << 6) | (7 << 3) | 7);
#endif
if (r)
{
/* Don't error out if another instance of dmd just created
* this directory
*/
if (errno != EEXIST)
return true;
}
}
}
}
return false;
}
/******************************************
* Return canonical version of name in a malloc'd buffer.
* This code is high risk.
*/
const char *FileName::canonicalName(const char *name)
{
#if POSIX
// NULL destination buffer is allowed and preferred
return realpath(name, NULL);
#elif _WIN32
/* Apparently, there is no good way to do this on Windows.
* GetFullPathName isn't it, but use it anyway.
*/
DWORD result = GetFullPathNameA(name, 0, NULL, NULL);
if (result)
{
char *buf = (char *)mem.xmalloc(result);
result = GetFullPathNameA(name, result, buf, NULL);
if (result == 0)
{
::free(buf);
return NULL;
}
return buf;
}
return NULL;
#else
assert(0);
return NULL;
#endif
}
/********************************
* Free memory allocated by FileName routines
*/
void FileName::free(const char *str)
{
if (str)
{ assert(str[0] != (char)0xAB);
memset(const_cast<char *>(str), 0xAB, strlen(str) + 1); // stomp
}
mem.xfree(const_cast<char *>(str));
}
const char *FileName::toChars() const
{
return str;
}