diff --git a/include/bx/filepath.h b/include/bx/filepath.h new file mode 100644 index 000000000..d844e07db --- /dev/null +++ b/include/bx/filepath.h @@ -0,0 +1,64 @@ +/* + * Copyright 2010-2017 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bx#license-bsd-2-clause + */ + +#ifndef BX_FILEPATH_H_HEADER_GUARD +#define BX_FILEPATH_H_HEADER_GUARD + +#include "string.h" + +namespace bx +{ + const int32_t kMaxFilePath = 1024; + + /// FilePath parser and helper. + /// + /// /abv/gd/555/333/pod.mac + /// ppppppppppppppppbbbeeee + /// ^ ^ ^ + /// +-path base-+ +-ext + /// ^^^^^^^ + /// +-filename + /// + class FilePath + { + public: + /// + FilePath(); + + /// + FilePath(const StringView& _str); + + /// + void set(const StringView& _str); + + /// + const StringView get() const; + + /// If path is `/abv/gd/555/333/pod.mac` returns `/abv/gd/555/333/`. + /// + const StringView getPath() const; + + /// If path is `/abv/gd/555/333/pod.mac` returns `pod.mac`. + /// + const StringView getFileName() const; + + /// If path is `/abv/gd/555/333/pod.mac` returns `pod`. + /// + const StringView getBaseName() const; + + /// If path is `/abv/gd/555/333/pod.mac` returns `.mac`. + /// + const StringView getExt() const; + + /// + bool isAbsolute() const; + + private: + char m_filePath[kMaxFilePath]; + }; + +} // namespace bx + +#endif // BX_FILEPATH_H_HEADER_GUARD diff --git a/include/bx/inline/readerwriter.inl b/include/bx/inline/readerwriter.inl index c2eb72a9e..72d96510d 100644 --- a/include/bx/inline/readerwriter.inl +++ b/include/bx/inline/readerwriter.inl @@ -293,6 +293,11 @@ namespace bx return _writer->write(_data, _size, _err); } + inline int32_t write(WriterI* _writer, const char* _str, Error* _err) + { + return write(_writer, _str, strLen(_str), _err); + } + inline int32_t writeRep(WriterI* _writer, uint8_t _byte, int32_t _size, Error* _err) { BX_ERROR_SCOPE(_err); diff --git a/include/bx/inline/string.inl b/include/bx/inline/string.inl index 033541023..46f2becd0 100644 --- a/include/bx/inline/string.inl +++ b/include/bx/inline/string.inl @@ -75,6 +75,11 @@ namespace bx set(_ptr, _len); } + inline StringView::StringView(const char* _ptr, const char* _term) + { + set(_ptr, _term); + } + inline void StringView::set(const char* _ptr, int32_t _len) { clear(); @@ -90,6 +95,11 @@ namespace bx } } + inline void StringView::set(const char* _ptr, const char* _term) + { + set(_ptr, int32_t(_term-_ptr) ); + } + inline void StringView::clear() { m_ptr = ""; diff --git a/include/bx/readerwriter.h b/include/bx/readerwriter.h index 0866c8835..d5c6dcede 100644 --- a/include/bx/readerwriter.h +++ b/include/bx/readerwriter.h @@ -247,6 +247,9 @@ namespace bx /// Write data. int32_t write(WriterI* _writer, const void* _data, int32_t _size, Error* _err = NULL); + /// Writer string. + inline int32_t write(WriterI* _writer, const char* _str, Error* _err = NULL); + /// Write repeat the same value. int32_t writeRep(WriterI* _writer, uint8_t _byte, int32_t _size, Error* _err = NULL); diff --git a/include/bx/string.h b/include/bx/string.h index 90875dfa6..3cdafa816 100644 --- a/include/bx/string.h +++ b/include/bx/string.h @@ -37,9 +37,15 @@ namespace bx /// StringView(const char* _ptr, int32_t _len = INT32_MAX); + /// + StringView(const char* _ptr, const char* _term); + /// void set(const char* _ptr, int32_t _len = INT32_MAX); + /// + void set(const char* _ptr, const char* _term); + /// void clear(); @@ -138,9 +144,15 @@ namespace bx /// String compare. int32_t strCmp(const char* _lhs, const char* _rhs, int32_t _max = INT32_MAX); + /// String compare. + int32_t strCmp(const char* _lhs, const StringView& _rhs); + /// Case insensitive string compare. int32_t strCmpI(const char* _lhs, const char* _rhs, int32_t _max = INT32_MAX); + /// Case insensitive string compare. + int32_t strCmpI(const char* _lhs, const StringView& _rhs); + /// Get string length. int32_t strLen(const char* _str, int32_t _max = INT32_MAX); @@ -148,9 +160,15 @@ namespace bx /// including zero terminator. Copy will be terminated with '\0'. int32_t strCopy(char* _dst, int32_t _dstSize, const char* _src, int32_t _num = INT32_MAX); + /// + int32_t strCopy(char* _dst, int32_t _dstSize, const StringView& _str); + /// Concatinate string. int32_t strCat(char* _dst, int32_t _dstSize, const char* _src, int32_t _num = INT32_MAX); + /// + int32_t strCat(char* _dst, int32_t _dstSize, const StringView& _str); + /// Find character in string. Limit search to _max characters. const char* strFind(const char* _str, char _ch, int32_t _max = INT32_MAX); @@ -218,9 +236,6 @@ namespace bx template Ty replaceAll(const Ty& _str, const char* _from, const char* _to); - /// Extract base file name from file path. - const char* baseName(const char* _filePath); - /// Convert size in bytes to human readable string kibi units. int32_t prettify(char* _out, int32_t _count, uint64_t _size, Units::Enum _units = Units::Kibi); diff --git a/src/filepath.cpp b/src/filepath.cpp new file mode 100644 index 000000000..88ddb936c --- /dev/null +++ b/src/filepath.cpp @@ -0,0 +1,212 @@ +/* + * Copyright 2010-2017 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bx#license-bsd-2-clause + */ + +#include +#include + +namespace bx +{ + static bool isPathSeparator(char _ch) + { + return false + || '/' == _ch + || '\\' == _ch + ; + } + + static int32_t normalizeFilePath(char* _dst, int32_t _dstSize, const char* _src, int32_t _num) + { + // Reference: Lexical File Names in Plan 9 or Getting Dot-Dot Right + // https://9p.io/sys/doc/lexnames.html + + const int32_t num = strLen(_src, _num); + + if (0 == num) + { + return strCopy(_dst, _dstSize, "."); + } + + int32_t size = 0; + + StaticMemoryBlockWriter writer(_dst, _dstSize); + Error err; + + int32_t idx = 0; + int32_t dotdot = 0; + + if (2 <= num + && ':' == _src[1]) + { + size += write(&writer, toUpper(_src[idx]), &err); + size += write(&writer, ':', &err); + idx += 2; + dotdot = size; + } + + const int32_t slashIdx = idx; + + bool rooted = isPathSeparator(_src[idx]); + if (rooted) + { + size += write(&writer, '/', &err); + ++idx; + dotdot = size; + } + + while (idx < num && err.isOk() ) + { + switch (_src[idx]) + { + case '/': + case '\\': + ++idx; + break; + + case '.': + if (idx+1 == num + || isPathSeparator(_src[idx+1]) ) + { + ++idx; + break; + } + + if ('.' == _src[idx+1] + && (idx+2 == num || isPathSeparator(_src[idx+2]) ) ) + { + idx += 2; + + if (dotdot < size) + { + for (--size + ; dotdot < size && !isPathSeparator(_dst[size]) + ; --size) + { + } + seek(&writer, size, Whence::Begin); + } + else if (!rooted) + { + if (0 < size) + { + size += write(&writer, '/', &err); + } + + size += write(&writer, "..", &err); + dotdot = size; + } + + break; + } + + BX_FALLTHROUGH; + + default: + if ( ( rooted && slashIdx+1 != size) + || (!rooted && 0 != size) ) + { + size += write(&writer, '/', &err); + } + + for (; idx < num && !isPathSeparator(_src[idx]); ++idx) + { + size += write(&writer, _src[idx], &err); + } + + break; + } + } + + if (0 == size) + { + size += write(&writer, '.', &err); + } + + write(&writer, '\0', &err); + + return size; + } + + FilePath::FilePath() + { + set(""); + } + + FilePath::FilePath(const StringView& _filePath) + { + set(_filePath); + } + + void FilePath::set(const StringView& _filePath) + { + normalizeFilePath( + m_filePath + , BX_COUNTOF(m_filePath) + , _filePath.getPtr() + , _filePath.getLength() + ); + } + + const StringView FilePath::get() const + { + return StringView(m_filePath); + } + + const StringView FilePath::getPath() const + { + const char* end = strRFind(m_filePath, '/'); + if (NULL != end) + { + return StringView(m_filePath, end+1); + } + + return StringView(); + } + + const StringView FilePath::getFileName() const + { + const char* fileName = strRFind(m_filePath, '/'); + if (NULL != fileName) + { + return StringView(fileName+1); + } + + return get(); + } + + const StringView FilePath::getBaseName() const + { + const StringView fileName = getFileName(); + if (!fileName.isEmpty() ) + { + const char* ext = strFind(fileName.getPtr(), '.', fileName.getLength() ); + if (ext != NULL) + { + return StringView(fileName.getPtr(), ext); + } + } + + return StringView(); + } + + const StringView FilePath::getExt() const + { + const StringView fileName = getFileName(); + if (!fileName.isEmpty() ) + { + const char* ext = strFind(fileName.getPtr(), '.', fileName.getLength() ); + return StringView(ext); + } + + return StringView(); + } + + bool FilePath::isAbsolute() const + { + return '/' == m_filePath[0] // no drive letter + || (':' == m_filePath[1] && '/' == m_filePath[2]) // with drive letter + ; + } + +} // namespace bx diff --git a/src/string.cpp b/src/string.cpp index cb724538f..2b959c3cb 100644 --- a/src/string.cpp +++ b/src/string.cpp @@ -134,11 +134,21 @@ namespace bx return strCmp(_lhs, _rhs, _max); } + int32_t strCmp(const char* _lhs, const StringView& _rhs) + { + return strCmp(_lhs, _rhs.getPtr(), _rhs.getLength() ); + } + int32_t strCmpI(const char* _lhs, const char* _rhs, int32_t _max) { return strCmp(_lhs, _rhs, _max); } + int32_t strCmpI(const char* _lhs, const StringView& _rhs) + { + return strCmpI(_lhs, _rhs.getPtr(), _rhs.getLength() ); + } + int32_t strLen(const char* _str, int32_t _max) { if (NULL == _str) @@ -166,6 +176,11 @@ namespace bx return num; } + int32_t strCopy(char* _dst, int32_t _dstSize, const StringView& _str) + { + return strCopy(_dst, _dstSize, _str.getPtr(), _str.getLength() ); + } + int32_t strCat(char* _dst, int32_t _dstSize, const char* _src, int32_t _num) { BX_CHECK(NULL != _dst, "_dst can't be NULL!"); @@ -177,6 +192,11 @@ namespace bx return strCopy(&_dst[len], max-len, _src, _num); } + int32_t strCat(char* _dst, int32_t _dstSize, const StringView& _str) + { + return strCat(_dst, _dstSize, _str.getPtr(), _str.getLength() ); + } + const char* strFind(const char* _str, char _ch, int32_t _max) { for (int32_t ii = 0, len = strLen(_str, _max); ii < len; ++ii) @@ -192,7 +212,7 @@ namespace bx const char* strRFind(const char* _str, char _ch, int32_t _max) { - for (int32_t ii = strLen(_str, _max); 0 < ii; --ii) + for (int32_t ii = strLen(_str, _max); 0 <= ii; --ii) { if (_str[ii] == _ch) { @@ -900,21 +920,6 @@ namespace bx return len; } - const char* baseName(const char* _filePath) - { - const char* bs = strRFind(_filePath, '\\'); - const char* fs = strRFind(_filePath, '/'); - const char* slash = (bs > fs ? bs : fs); - const char* colon = strRFind(_filePath, ':'); - const char* basename = slash > colon ? slash : colon; - if (NULL != basename) - { - return basename+1; - } - - return _filePath; - } - static const char s_units[] = { 'B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y' }; template diff --git a/tests/filepath_test.cpp b/tests/filepath_test.cpp new file mode 100644 index 000000000..70b32bd5b --- /dev/null +++ b/tests/filepath_test.cpp @@ -0,0 +1,118 @@ +/* + * Copyright 2010-2017 Branimir Karadzic. All rights reserved. + * License: https://github.com/bkaradzic/bx#license-bsd-2-clause + */ + +#include "test.h" +#include + +struct FilePathTest +{ + const char* filePath; + const char* expected; +}; + +FilePathTest s_filePathTest[] = +{ + // Already clean + {"", "."}, + {"abc", "abc"}, + {"abc/def", "abc/def"}, + {"a/b/c", "a/b/c"}, + {".", "."}, + {"..", ".."}, + {"../..", "../.."}, + {"../../abc", "../../abc"}, + {"/abc", "/abc"}, + {"/", "/"}, + + // Remove trailing slash + {"abc/", "abc"}, + {"abc/def/", "abc/def"}, + {"a/b/c/", "a/b/c"}, + {"./", "."}, + {"../", ".."}, + {"../../", "../.."}, + {"/abc/", "/abc"}, + + // Remove doubled slash + {"abc//def//ghi", "abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc"}, + {"abc//", "abc"}, + + // Remove . elements + {"abc/./def", "abc/def"}, + {"/./abc/def", "/abc/def"}, + {"abc/.", "abc"}, + + // Remove .. elements + {"abc/def/ghi/../jkl", "abc/def/jkl"}, + {"abc/def/../ghi/../jkl", "abc/jkl"}, + {"abc/def/..", "abc"}, + {"abc/def/../..", "."}, + {"/abc/def/../..", "/"}, + {"abc/def/../../..", ".."}, + {"/abc/def/../../..", "/"}, + {"abc/def/../../../ghi/jkl/../../../mno", "../../mno"}, + + // Combinations + {"abc/./../def", "def"}, + {"abc//./../def", "def"}, + {"abc/../../././../def", "../../def"}, + + {"abc\\/../..\\/././../def", "../../def"}, + {"\\abc/def\\../..\\..", "/"}, +}; + +struct FilePathSplit +{ + const char* filePath; + bool absolute; + const char* path; + const char* fileName; + const char* baseName; + const char* extension; +}; + +static const FilePathSplit s_filePathSplit[] = +{ + { "\\abc/def\\../..\\../test.txt", true, "/", "test.txt", "test", ".txt" }, + { "/abv/gd/555/333/pod.mac", true, "/abv/gd/555/333/", "pod.mac", "pod", ".mac" }, + { "archive.tar.gz", false, "", "archive.tar.gz", "archive", ".tar.gz" }, + { "tmp/archive.tar.gz", false, "tmp/", "archive.tar.gz", "archive", ".tar.gz" }, + { "/tmp/archive.tar.gz", true, "/tmp/", "archive.tar.gz", "archive", ".tar.gz" }, + { "d:/tmp/archive.tar.gz", true, "D:/tmp/", "archive.tar.gz", "archive", ".tar.gz" }, +}; + +TEST_CASE("FilePath", "") +{ + bx::FilePath fp; + for (uint32_t ii = 0; ii < BX_COUNTOF(s_filePathTest); ++ii) + { + const FilePathTest& test = s_filePathTest[ii]; + + fp.set(test.filePath); + const bx::StringView result = fp.get(); + + REQUIRE(0 == bx::strCmp(test.expected, result) ); + } + + for (uint32_t ii = 0; ii < BX_COUNTOF(s_filePathSplit); ++ii) + { + const FilePathSplit& test = s_filePathSplit[ii]; + + fp.set(test.filePath); + const bx::StringView path = fp.getPath(); + const bx::StringView fileName = fp.getFileName(); + const bx::StringView baseName = fp.getBaseName(); + const bx::StringView ext = fp.getExt(); + + REQUIRE(0 == bx::strCmp(test.path, path) ); + REQUIRE(0 == bx::strCmp(test.fileName, fileName) ); + REQUIRE(0 == bx::strCmp(test.baseName, baseName) ); + REQUIRE(0 == bx::strCmp(test.extension, ext) ); + REQUIRE(test.absolute == fp.isAbsolute() ); + }; +}