mirror of
https://github.com/ParksProjets/Maconv.git
synced 2024-06-05 01:29:31 +00:00
738 lines
24 KiB
C++
738 lines
24 KiB
C++
/******************************************************************************
|
|
* Copyright (c) 2013 Dan Lecocq
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*****************************************************************************/
|
|
|
|
#ifndef APATHY__PATH_HPP
|
|
#define APATHY__PATH_HPP
|
|
|
|
/* C++ includes */
|
|
#include <vector>
|
|
#include <string>
|
|
#include <cstring>
|
|
#include <cstdlib>
|
|
#include <istream>
|
|
#include <sstream>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
|
|
/* C includes */
|
|
#include <glob.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
/* A class for path manipulation */
|
|
namespace apathy {
|
|
class Path {
|
|
public:
|
|
/* This is the separator used on this particular system */
|
|
#ifdef __MSDOS__
|
|
#error "Platforms using backslashes not yet supported"
|
|
#else
|
|
static const char separator = '/';
|
|
#endif
|
|
/* A class meant to contain path segments */
|
|
struct Segment {
|
|
/* The actual string segment */
|
|
std::string segment;
|
|
|
|
Segment(std::string s=""): segment(s) {}
|
|
|
|
friend std::istream& operator>>(std::istream& stream, Segment& s) {
|
|
return std::getline(stream, s.segment, separator);
|
|
}
|
|
};
|
|
|
|
/**********************************************************************
|
|
* Constructors
|
|
*********************************************************************/
|
|
|
|
/* Default constructor
|
|
*
|
|
* Points to current directory */
|
|
Path(const std::string& path=""): path(path) {}
|
|
|
|
/* Our generalized constructor.
|
|
*
|
|
* This enables all sorts of type promotion (like int -> Path) for
|
|
* arguments into all the functions below. Anything that
|
|
* std::stringstream can support is implicitly supported as well
|
|
*
|
|
* @param p - path to construct */
|
|
template <class T>
|
|
Path(const T& p);
|
|
|
|
/**********************************************************************
|
|
* Operators
|
|
*********************************************************************/
|
|
/* Checks if the paths are exactly the same */
|
|
bool operator==(const Path& other) { return path == other.path; }
|
|
|
|
/* Check if the paths are not exactly the same */
|
|
bool operator!=(const Path& other) { return ! (*this == other); }
|
|
|
|
/* Append the provided segment to the path as a directory. This is the
|
|
* same as append(segment)
|
|
*
|
|
* @param segment - path segment to add to this path */
|
|
Path& operator<<(const Path& segment);
|
|
|
|
/* Append the provided segment to the path as a directory. This is the
|
|
* same as append(segment). Returns a /new/ path object rather than a
|
|
* reference.
|
|
*
|
|
* @param segment - path segment to add to this path */
|
|
Path operator+(const Path& segment) const;
|
|
|
|
/* Check if the two paths are equivalent
|
|
*
|
|
* Two paths are equivalent if they point to the same resource, even if
|
|
* they are not exact string matches
|
|
*
|
|
* @param other - path to compare to */
|
|
bool equivalent(const Path& other);
|
|
|
|
/* Return a string version of this path */
|
|
std::string string() const { return path; }
|
|
|
|
/* Return the name of the file */
|
|
std::string filename() const;
|
|
|
|
/* Return the extension of the file */
|
|
std::string extension() const;
|
|
|
|
/* Return a path object without the extension */
|
|
Path stem() const;
|
|
|
|
/**********************************************************************
|
|
* Manipulations
|
|
*********************************************************************/
|
|
|
|
/* Append the provided segment to the path as a directory. Alias for
|
|
* `operator<<`
|
|
*
|
|
* @param segment - path segment to add to this path */
|
|
Path& append(const Path& segment);
|
|
|
|
/* Evaluate the provided path relative to this path. If the second path
|
|
* is absolute, then return the second path.
|
|
*
|
|
* @param rel - path relative to this path to evaluate */
|
|
Path& relative(const Path& rel);
|
|
|
|
/* Move up one level in the directory structure */
|
|
Path& up();
|
|
|
|
/* Turn this into an absolute path
|
|
*
|
|
* If the path is already absolute, it has no effect. Otherwise, it is
|
|
* evaluated relative to the current working directory */
|
|
Path& absolute();
|
|
|
|
/* Sanitize this path
|
|
*
|
|
* This...
|
|
*
|
|
* 1) replaces runs of consecutive separators with a single separator
|
|
* 2) evaluates '..' to refer to the parent directory, and
|
|
* 3) strips out '/./' as referring to the current directory
|
|
*
|
|
* If the path was absolute to begin with, it will be absolute
|
|
* afterwards. If it was a relative path to begin with, it will only be
|
|
* converted to an absolute path if it uses enough '..'s to refer to
|
|
* directories above the current working directory */
|
|
Path& sanitize();
|
|
|
|
/* Make this path a directory
|
|
*
|
|
* If this path does not have a trailing directory separator, add one.
|
|
* If it already does, this does not affect the path */
|
|
Path& directory();
|
|
|
|
/* Trim this path of trailing separators, up to the leading separator.
|
|
* For example, on *nix systems:
|
|
*
|
|
* assert(Path("///").trim() == "/");
|
|
* assert(Path("/foo//").trim() == "/foo");
|
|
*/
|
|
Path& trim();
|
|
|
|
/**********************************************************************
|
|
* Copiers
|
|
*********************************************************************/
|
|
|
|
/* Return parent path
|
|
*
|
|
* Returns a new Path object referring to the parent directory. To
|
|
* move _this_ path to the parent directory, use the `up` function */
|
|
Path parent() const { return Path(Path(*this).up()); }
|
|
|
|
/**********************************************************************
|
|
* Member Utility Methods
|
|
*********************************************************************/
|
|
|
|
/* Returns a vector of each of the path segments in this path */
|
|
std::vector<Segment> split() const;
|
|
|
|
/**********************************************************************
|
|
* Type Tests
|
|
*********************************************************************/
|
|
/* Is the path an absolute path? */
|
|
bool is_absolute() const;
|
|
|
|
/* Does the path have a trailing slash? */
|
|
bool trailing_slash() const;
|
|
|
|
/* Does this path exist?
|
|
*
|
|
* Returns true if the path can be `stat`d */
|
|
bool exists() const;
|
|
|
|
/* Is this path an existing file?
|
|
*
|
|
* Only returns true if the path has stat.st_mode that is a regular
|
|
* file */
|
|
bool is_file() const;
|
|
|
|
/* Is this path an existing directory?
|
|
*
|
|
* Only returns true if the path has a stat.st_mode that is a
|
|
* directory */
|
|
bool is_directory() const;
|
|
|
|
/* How large is this file?
|
|
*
|
|
* Returns the file size in bytes. If the file doesn't exist, it
|
|
* returns 0 */
|
|
size_t size() const;
|
|
|
|
/**********************************************************************
|
|
* Static Utility Methods
|
|
*********************************************************************/
|
|
|
|
/* Return a brand new path as the concatenation of the two provided
|
|
* paths
|
|
*
|
|
* @param a - first part of the path to join
|
|
* @param b - second part of the path to join
|
|
*/
|
|
static Path join(const Path& a, const Path& b);
|
|
|
|
/* Return a branch new path as the concatenation of each segments
|
|
*
|
|
* @param segments - the path segments to concatenate
|
|
*/
|
|
static Path join(const std::vector<Segment>& segments);
|
|
|
|
/* Current working directory */
|
|
static Path cwd();
|
|
|
|
/* Create a file if one does not exist
|
|
*
|
|
* @param p - path to create
|
|
* @param mode - mode to create with */
|
|
static bool touch(const Path& p, mode_t mode=0777);
|
|
|
|
/* Move / rename a file
|
|
*
|
|
* @param source - original path
|
|
* @param dest - new path
|
|
* @param mkdirs - recursively make any needed directories? */
|
|
static bool move(const Path& source, const Path& dest,
|
|
bool mkdirs=false);
|
|
|
|
/* Remove a file
|
|
*
|
|
* @param path - path to remove */
|
|
static bool rm(const Path& path);
|
|
|
|
/* Recursively make directories
|
|
*
|
|
* @param p - path to recursively make
|
|
* @returns true if it was able to, false otherwise */
|
|
static bool makedirs(const Path& p, mode_t mode=0777);
|
|
|
|
/* Recursively remove directories
|
|
*
|
|
* @param p - path to recursively remove */
|
|
static bool rmdirs(const Path& p, bool ignore_errors=false);
|
|
|
|
/* List all the paths in a directory
|
|
*
|
|
* @param p - path to list items for */
|
|
static std::vector<Path> listdir(const Path& p);
|
|
|
|
/* Returns all matching globs
|
|
*
|
|
* @param pattern - the glob pattern to match */
|
|
static std::vector<Path> glob(const std::string& pattern);
|
|
|
|
/* So that we can write paths out to ostreams */
|
|
friend std::ostream& operator<<(std::ostream& stream, const Path& p) {
|
|
return stream << p.path;
|
|
}
|
|
private:
|
|
/* Our current path */
|
|
std::string path;
|
|
};
|
|
|
|
/* Constructor */
|
|
template <class T>
|
|
inline Path::Path(const T& p): path("") {
|
|
std::stringstream ss;
|
|
ss << p;
|
|
path = ss.str();
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Operators
|
|
*************************************************************************/
|
|
inline Path& Path::operator<<(const Path& segment) {
|
|
return append(segment);
|
|
}
|
|
|
|
inline Path Path::operator+(const Path& segment) const {
|
|
Path result(path);
|
|
result.append(segment);
|
|
return result;
|
|
}
|
|
|
|
inline bool Path::equivalent(const Path& other) {
|
|
/* Make copies of both paths, sanitize, and ensure they're equal */
|
|
return Path(path).absolute().sanitize() ==
|
|
Path(other).absolute().sanitize();
|
|
}
|
|
|
|
inline std::string Path::filename() const {
|
|
size_t pos = path.rfind(separator);
|
|
if (pos != std::string::npos) {
|
|
return path.substr(pos + 1);
|
|
}
|
|
return path; // FIXED -- Filename for relative files
|
|
}
|
|
|
|
inline std::string Path::extension() const {
|
|
/* Make sure we only look in the filename, and not the path */
|
|
std::string name = filename();
|
|
size_t pos = name.rfind('.');
|
|
if (pos != std::string::npos) {
|
|
return name.substr(pos + 1);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
inline Path Path::stem() const {
|
|
size_t sep_pos = path.rfind(separator);
|
|
size_t dot_pos = path.rfind('.');
|
|
if (dot_pos == std::string::npos) {
|
|
return Path(*this);
|
|
}
|
|
|
|
if (sep_pos == std::string::npos || sep_pos < dot_pos) {
|
|
return Path(path.substr(0, dot_pos));
|
|
} else {
|
|
return Path(*this);
|
|
}
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Manipulators
|
|
*************************************************************************/
|
|
inline Path& Path::append(const Path& segment) {
|
|
/* First, check if the last character is the separator character.
|
|
* If not, then append one and then the segment. Otherwise, just
|
|
* the segment */
|
|
if (!trailing_slash()) {
|
|
path.push_back(separator);
|
|
}
|
|
path.append(segment.path);
|
|
return *this;
|
|
}
|
|
|
|
inline Path& Path::relative(const Path& rel) {
|
|
if (!rel.is_absolute()) {
|
|
return append(rel);
|
|
} else {
|
|
operator=(rel);
|
|
return *this;
|
|
}
|
|
}
|
|
|
|
inline Path& Path::up() {
|
|
/* Make sure we turn this into an absolute url if it's not already
|
|
* one */
|
|
if (path.size() == 0) {
|
|
path = "..";
|
|
return directory();
|
|
}
|
|
|
|
append("..").sanitize();
|
|
if (path.size() == 0) {
|
|
return *this;
|
|
}
|
|
return directory();
|
|
}
|
|
|
|
inline Path& Path::absolute() {
|
|
/* If the path doesn't begin with our separator, then it's not an
|
|
* absolute path, and should be appended to the current working
|
|
* directory */
|
|
if (!is_absolute()) {
|
|
/* Join our current working directory with the path */
|
|
operator=(join(cwd(), path));
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
inline Path& Path::sanitize() {
|
|
/* Split the path up into segments */
|
|
std::vector<Segment> segments(split());
|
|
/* We may have to test this repeatedly, so let's check once */
|
|
bool relative = !is_absolute();
|
|
|
|
/* Now, we'll create a new set of segments */
|
|
std::vector<Segment> pruned;
|
|
for (size_t pos = 0; pos < segments.size(); ++pos) {
|
|
/* Skip over empty segments and '.' */
|
|
if (segments[pos].segment.size() == 0 ||
|
|
segments[pos].segment == ".") {
|
|
continue;
|
|
}
|
|
|
|
/* If there is a '..', then pop off a parent directory. However, if
|
|
* the path was relative to begin with, if the '..'s exceed the
|
|
* stack depth, then they should be appended to our path. If it was
|
|
* absolute to begin with, and we reach root, then '..' has no
|
|
* effect */
|
|
if (segments[pos].segment == "..") {
|
|
if (relative) {
|
|
if (pruned.size() && pruned.back().segment != "..") {
|
|
pruned.pop_back();
|
|
} else {
|
|
pruned.push_back(segments[pos]);
|
|
}
|
|
} else if (pruned.size()) {
|
|
pruned.pop_back();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
pruned.push_back(segments[pos]);
|
|
}
|
|
|
|
bool was_directory = trailing_slash();
|
|
if (!relative) {
|
|
path = std::string(1, separator) + Path::join(pruned).path;
|
|
if (was_directory) {
|
|
return directory();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/* It was a relative path */
|
|
path = Path::join(pruned).path;
|
|
if (path.length() && was_directory) {
|
|
return directory();
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
inline Path& Path::directory() {
|
|
trim();
|
|
path.push_back(separator);
|
|
return *this;
|
|
}
|
|
|
|
inline Path& Path::trim() {
|
|
if (path.length() == 0) { return *this; }
|
|
|
|
size_t p = path.find_last_not_of(separator);
|
|
if (p != std::string::npos) {
|
|
path.erase(p + 1, path.size());
|
|
} else {
|
|
path = "";
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Member Utility Methods
|
|
*************************************************************************/
|
|
|
|
/* Returns a vector of each of the path segments in this path */
|
|
inline std::vector<Path::Segment> Path::split() const {
|
|
std::stringstream stream(path);
|
|
std::istream_iterator<Path::Segment> start(stream);
|
|
std::istream_iterator<Path::Segment> end;
|
|
std::vector<Path::Segment> results(start, end);
|
|
if (trailing_slash()) {
|
|
results.push_back(Path::Segment(""));
|
|
}
|
|
return results;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Tests
|
|
*************************************************************************/
|
|
inline bool Path::is_absolute() const {
|
|
return path.size() && path[0] == separator;
|
|
}
|
|
|
|
inline bool Path::trailing_slash() const {
|
|
return path.size() && path[path.length() - 1] == separator;
|
|
}
|
|
|
|
inline bool Path::exists() const {
|
|
struct stat buf;
|
|
if (stat(path.c_str(), &buf) != 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline bool Path::is_file() const {
|
|
struct stat buf;
|
|
if (stat(path.c_str(), &buf) != 0) {
|
|
return false;
|
|
} else {
|
|
return S_ISREG(buf.st_mode);
|
|
}
|
|
}
|
|
|
|
inline bool Path::is_directory() const {
|
|
struct stat buf;
|
|
if (stat(path.c_str(), &buf) != 0) {
|
|
return false;
|
|
} else {
|
|
return S_ISDIR(buf.st_mode);
|
|
}
|
|
}
|
|
|
|
inline size_t Path::size() const {
|
|
struct stat buf;
|
|
if (stat(path.c_str(), &buf) != 0) {
|
|
return 0;
|
|
} else {
|
|
return buf.st_size;
|
|
}
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Static Utility Methods
|
|
*************************************************************************/
|
|
inline Path Path::join(const Path& a, const Path& b) {
|
|
Path p(a);
|
|
p.append(b);
|
|
return p;
|
|
}
|
|
|
|
inline Path Path::join(const std::vector<Segment>& segments) {
|
|
std::string path;
|
|
/* Now, we'll go through the segments, and join them with
|
|
* separator */
|
|
std::vector<Segment>::const_iterator it(segments.begin());
|
|
for(; it != segments.end(); ++it) {
|
|
path += it->segment;
|
|
if (it + 1 != segments.end()) {
|
|
path += std::string(1, separator);
|
|
}
|
|
}
|
|
return Path(path);
|
|
}
|
|
|
|
inline Path Path::cwd() {
|
|
Path p;
|
|
|
|
char * buf = getcwd(NULL, 0);
|
|
if (buf != NULL) {
|
|
p = std::string(buf);
|
|
free(buf);
|
|
} else {
|
|
perror("cwd");
|
|
}
|
|
|
|
/* Ensure this is a directory */
|
|
p.directory();
|
|
return p;
|
|
}
|
|
|
|
inline bool Path::touch(const Path& p, mode_t mode) {
|
|
int fd = open(p.path.c_str(), O_RDONLY | O_CREAT, mode);
|
|
if (fd == -1) {
|
|
makedirs(p);
|
|
fd = open(p.path.c_str(), O_RDONLY | O_CREAT, mode);
|
|
if (fd == -1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (close(fd) == -1) {
|
|
perror("touch close");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
inline bool Path::move(const Path& source, const Path& dest,
|
|
bool mkdirs) {
|
|
int result = rename(source.path.c_str(), dest.path.c_str());
|
|
if (result == 0) {
|
|
return true;
|
|
}
|
|
|
|
/* Otherwise, there was an error */
|
|
if (errno == ENOENT && mkdirs) {
|
|
makedirs(dest.parent());
|
|
return rename(source.path.c_str(), dest.path.c_str()) == 0;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
inline bool Path::rm(const Path& path) {
|
|
if (remove(path.path.c_str()) != 0) {
|
|
perror("Remove");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
inline bool Path::makedirs(const Path& p, mode_t mode) {
|
|
/* We need to make a copy of the path, that's an absolute path */
|
|
Path abs = Path(p).absolute();
|
|
|
|
/* Now, we'll try to make the directory / ensure it exists */
|
|
if (mkdir(abs.string().c_str(), mode) == 0) {
|
|
return true;
|
|
}
|
|
|
|
/* Otherwise, there was an error. There are some errors that
|
|
* may be recoverable */
|
|
if (errno == EEXIST) {
|
|
return abs.is_directory();
|
|
} else if(errno == ENOENT) {
|
|
/* We'll need to try to recursively make this directory. We
|
|
* don't need to worry about reaching the '/' path, and then
|
|
* getting to this point, because / always exists */
|
|
makedirs(abs.parent(), mode);
|
|
if (mkdir(abs.string().c_str(), mode) == 0) {
|
|
return true;
|
|
} else {
|
|
perror("makedirs");
|
|
return false;
|
|
}
|
|
} else {
|
|
perror("makedirs");
|
|
}
|
|
|
|
/* If it's none of these cases, then it's one of unrecoverable
|
|
* errors described in mkdir(2) */
|
|
return false;
|
|
}
|
|
|
|
inline bool Path::rmdirs(const Path& p, bool ignore_errors) {
|
|
/* If this path isn't a file, then complain */
|
|
if (!p.is_directory()) {
|
|
return false;
|
|
}
|
|
|
|
/* First, we list out all the members of the path, and anything
|
|
* that's a directory, we rmdirs(...) it. If it's a file, then we
|
|
* remove it */
|
|
std::vector<Path> subdirs(listdir(p));
|
|
std::vector<Path>::iterator it(subdirs.begin());
|
|
for (; it != subdirs.end(); ++it) {
|
|
if (it->is_directory() && !rmdirs(*it) && !ignore_errors) {
|
|
std::cout << "Failed rmdirs " << it->string() << std::endl;
|
|
} else if (it->is_file() &&
|
|
remove(it->path.c_str()) != 0 && !ignore_errors) {
|
|
std::cout << "Failed remove " << it->string() << std::endl;
|
|
}
|
|
}
|
|
|
|
/* Lastly, try to remove the directory itself */
|
|
bool result = (remove(p.path.c_str()) == 0);
|
|
errno = 0;
|
|
return result;
|
|
}
|
|
|
|
/* List all the paths in a directory
|
|
*
|
|
* @param p - path to list items for */
|
|
inline std::vector<Path> Path::listdir(const Path& p) {
|
|
Path base(p);
|
|
base.absolute();
|
|
std::vector<Path> results;
|
|
DIR* dir = opendir(base.string().c_str());
|
|
if (dir == NULL) {
|
|
/* If there was an error, return an empty vector */
|
|
return results;
|
|
}
|
|
|
|
/* Otherwise, go through everything */
|
|
for (dirent* ent = readdir(dir); ent != NULL; ent = readdir(dir)) {
|
|
Path cpy(base);
|
|
|
|
/* Skip the parent directory listing */
|
|
if (!strcmp(ent->d_name, "..")) {
|
|
continue;
|
|
}
|
|
|
|
/* Skip the self directory listing */
|
|
if (!strcmp(ent->d_name, ".")) {
|
|
continue;
|
|
}
|
|
|
|
cpy.relative(ent->d_name);
|
|
results.push_back(cpy);
|
|
}
|
|
|
|
errno = 0;
|
|
closedir(dir);
|
|
return results;
|
|
}
|
|
|
|
inline std::vector<Path> Path::glob(const std::string& pattern) {
|
|
/* First, we need a glob_t, and then we'll look at the results */
|
|
glob_t globbuf;
|
|
if (::glob(pattern.c_str(), 0, NULL, &globbuf) != 0) {
|
|
/* Then there was an error */
|
|
return std::vector<Path>();
|
|
}
|
|
|
|
std::vector<Path> results;
|
|
for (std::size_t i = 0; i < globbuf.gl_pathc; ++i) {
|
|
results.push_back(globbuf.gl_pathv[i]);
|
|
}
|
|
return results;
|
|
}
|
|
}
|
|
|
|
|
|
// Include "Path" class in global namespace.
|
|
using apathy::Path;
|
|
|
|
#endif |