F´ Flight Software - C/C++ Documentation
A framework for building embedded system applications to NASA flight quality standards.
FilePathUtils.cpp
Go to the documentation of this file.
1 // ======================================================================
2 // \title Os/FilePathUtils.cpp
3 // \brief Implementation of file path utilities
4 // ======================================================================
5 #include <Fw/Types/Assert.hpp>
7 #include <Os/FilePathUtils.hpp>
8 #include <Os/FileSystem.hpp>
9 #include <cstring>
10 
11 namespace Os {
12 namespace FilePathUtils {
13 
14 // Helper: validate that string_length did not return the buffer size
15 // (which indicates no null terminator was found — an invalid string).
16 static bool isValidCString(FwSizeType measuredLen, FwSizeType bufferLen) {
17  return measuredLen < bufferLen;
18 }
19 
20 // Helper: copy the raw (unresolved) absolute path into resolvedOut.
21 // If path is relative, it is prepended with baseDir.
22 static Status buildAbsolutePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) {
23  FW_ASSERT(path != nullptr);
24  FW_ASSERT(resolvedOut != nullptr);
25 
26  if (path[0] != '/') {
27  // Relative path: baseDir is required and must start with '/'
28  if (baseDir == nullptr) {
29  return INVALID_PATH;
30  }
31  const FwSizeType baseDirLen = Fw::StringUtils::string_length(baseDir, MAX_PATH_LENGTH);
32  if (!isValidCString(baseDirLen, MAX_PATH_LENGTH)) {
33  return INVALID_PATH;
34  }
35  if (baseDirLen == 0 || baseDir[0] != '/') {
36  return INVALID_PATH;
37  }
39  if (!isValidCString(pathLen, MAX_PATH_LENGTH)) {
40  return INVALID_PATH;
41  }
42  const FwSizeType needsSlash = (baseDir[baseDirLen - 1] != '/') ? 1 : 0;
43  if ((baseDirLen + needsSlash + pathLen + 1) > resolvedSize) {
44  return TOO_LONG;
45  }
46  (void)std::memcpy(resolvedOut, baseDir, baseDirLen);
47  FwSizeType pos = baseDirLen;
48  if (needsSlash) {
49  resolvedOut[pos++] = '/';
50  }
51  (void)std::memcpy(&resolvedOut[pos], path, pathLen);
52  pos += pathLen;
53  resolvedOut[pos] = '\0';
54  } else {
56  if (!isValidCString(pathLen, MAX_PATH_LENGTH)) {
57  return INVALID_PATH;
58  }
59  if (pathLen + 1 > resolvedSize) {
60  return TOO_LONG;
61  }
62  (void)std::memcpy(resolvedOut, path, pathLen + 1);
63  }
64  return VALID;
65 }
66 
67 // Helper: resolve `.`, `..`, and `//` in place within resolvedOut.
68 //
69 // Algorithm: single-pass using read (readPos) and write (writePos) pointers
70 // on the same buffer. Since resolving only shrinks or maintains length,
71 // writePos <= readPos always holds, making memmove safe for overlapping regions.
72 //
73 // For each path segment between '/' separators:
74 // - Empty or `.` segments are skipped (no-op)
75 // - `..` segments back up writePos to the previous '/'
76 // - Normal segments are shifted left (via memmove) to writePos
77 static Status resolveInPlace(char* resolvedOut) {
78  FW_ASSERT(resolvedOut != nullptr);
79  FW_ASSERT(resolvedOut[0] == '/');
80 
81  const FwSizeType pathLength = Fw::StringUtils::string_length(resolvedOut, MAX_PATH_LENGTH);
82  if (!isValidCString(pathLength, MAX_PATH_LENGTH)) {
83  return INVALID_PATH;
84  }
85 
86  FwSizeType writePos = 1; // position past root '/'
87 
88  for (FwSizeType readPos = 1; readPos <= pathLength;) {
89  // Mark start of current segment
90  FwSizeType segStart = readPos;
91 
92  // Scan forward to find the end of this segment (next '/' or end of string)
93  for (; readPos < pathLength && resolvedOut[readPos] != '/'; readPos++) {
94  }
95  FwSizeType segLen = readPos - segStart;
96 
97  // Advance readPos past the '/' separator (or force exit at end)
98  if (readPos < pathLength) {
99  readPos++;
100  } else {
101  readPos = pathLength + 1;
102  }
103 
104  // Skip empty segments (caused by consecutive slashes like "//")
105  if (segLen == 0) {
106  continue;
107  }
108  // Skip `.` segments (current directory, no-op)
109  if (segLen == 1 && resolvedOut[segStart] == '.') {
110  continue;
111  }
112  // Handle `..` segments by backing up to the previous directory
113  if (segLen == 2 && resolvedOut[segStart] == '.' && resolvedOut[segStart + 1] == '.') {
114  if (writePos > 1) {
115  writePos--;
116  for (; writePos > 1 && resolvedOut[writePos - 1] != '/'; writePos--) {
117  }
118  }
119  continue;
120  }
121 
122  // Normal segment: shift left to writePos
123  (void)std::memmove(&resolvedOut[writePos], &resolvedOut[segStart], segLen);
124  writePos += segLen;
125  resolvedOut[writePos++] = '/';
126  }
127 
128  // Remove trailing '/' (unless path is root "/")
129  if (writePos > 1) {
130  writePos--;
131  }
132  resolvedOut[writePos] = '\0';
133  return VALID;
134 }
135 
136 Status resolvePath(const char* path, const char* baseDir, char* resolvedOut, FwSizeType resolvedSize) {
137  FW_ASSERT(resolvedOut != nullptr);
138  if (path == nullptr || path[0] == '\0') {
139  return INVALID_PATH;
140  }
141 
142  const Status buildStatus = buildAbsolutePath(path, baseDir, resolvedOut, resolvedSize);
143  if (buildStatus != VALID) {
144  return buildStatus;
145  }
146  return resolveInPlace(resolvedOut);
147 }
148 
149 Status resolvePath(const Fw::ConstStringBase& path, const Fw::ConstStringBase& baseDir, Fw::StringBase& resolvedOut) {
150  // SAFETY: write directly into resolvedOut's internal buffer to avoid a temporary copy.
151  // StringBase::toChar() returns a pointer to the internal buffer, and resolvePath always
152  // null-terminates the output, keeping the StringBase in a valid state.
153  char* outBuffer = const_cast<char*>(resolvedOut.toChar());
154  // Cap at MAX_PATH_LENGTH so resolveInPlace's string_length scan stays consistent
155  const FwSizeType capacity = static_cast<FwSizeType>(resolvedOut.getCapacity());
156  const FwSizeType resolvedSize = (capacity > MAX_PATH_LENGTH) ? MAX_PATH_LENGTH : capacity;
157  return resolvePath(path.toChar(), baseDir.toChar(), outBuffer, resolvedSize);
158 }
159 
160 Status resolveFromCwd(const char* path, char* resolvedOut, FwSizeType resolvedSize) {
161  FW_ASSERT(path != nullptr);
162  FW_ASSERT(resolvedOut != nullptr);
163 
164  if (path[0] != '/') {
165  char cwdBuffer[MAX_PATH_LENGTH];
167  if (cwdStatus != Os::FileSystem::Status::OP_OK) {
168  return INVALID_PATH;
169  }
170  return resolvePath(path, cwdBuffer, resolvedOut, resolvedSize);
171  }
172  return resolvePath(path, "/", resolvedOut, resolvedSize);
173 }
174 
175 // Internal containment check on already-resolved paths.
176 // Verifies that resolvedPath starts with allowedDirectory as a prefix,
177 // and that the match occurs at a `/` boundary.
178 Status checkContainment(const char* resolvedPath, const char* allowedDirectory) {
179  FW_ASSERT(resolvedPath != nullptr);
180  FW_ASSERT(allowedDirectory != nullptr);
181 
182  const FwSizeType allowedLen = Fw::StringUtils::string_length(allowedDirectory, MAX_PATH_LENGTH);
183  const FwSizeType pathLen = Fw::StringUtils::string_length(resolvedPath, MAX_PATH_LENGTH);
184 
185  if (!isValidCString(allowedLen, MAX_PATH_LENGTH) || !isValidCString(pathLen, MAX_PATH_LENGTH)) {
186  return INVALID_PATH;
187  }
188  if (allowedLen == 0 || pathLen == 0) {
189  return OUTSIDE_SANDBOX;
190  }
191  if (allowedDirectory[allowedLen - 1] != '/') {
192  return OUTSIDE_SANDBOX;
193  }
194 
195  // Special case: path equals the allowed directory without trailing '/'.
196  // e.g. path="/data/uplink" matches allowedDirectory="/data/uplink/"
197  // This is valid because the path IS the sandbox directory itself.
198  if (pathLen == allowedLen - 1 && std::memcmp(resolvedPath, allowedDirectory, pathLen) == 0) {
199  return VALID;
200  }
201 
202  // Normal case: path must be at least as long as allowed and start with it
203  if (pathLen < allowedLen) {
204  return OUTSIDE_SANDBOX;
205  }
206  if (std::memcmp(resolvedPath, allowedDirectory, allowedLen) != 0) {
207  return OUTSIDE_SANDBOX;
208  }
209  return VALID;
210 }
211 
212 } // namespace FilePathUtils
213 } // namespace Os
Resolved path falls outside the allowed directory.
Operation succeeded.
Definition: Os.hpp:27
static bool isValidCString(FwSizeType measuredLen, FwSizeType bufferLen)
PlatformSizeType FwSizeType
Path is malformed or cannot be resolved.
virtual const CHAR * toChar() const =0
Convert to a C-style char*.
Status checkContainment(const char *resolvedPath, const char *allowedDirectory)
Check whether an already-resolved path is within an allowed directory.
static Status resolveInPlace(char *resolvedOut)
static Status buildAbsolutePath(const char *path, const char *baseDir, char *resolvedOut, FwSizeType resolvedSize)
static constexpr FwSizeType MAX_PATH_LENGTH
Maximum supported path length for resolution buffers.
virtual SizeType getCapacity() const =0
Return the size of the buffer.
static Status getWorkingDirectory(char *path, FwSizeType bufferSize)
Get the current working directory.
Definition: FileSystem.cpp:95
Path is valid and within the allowed directory.
Status resolveFromCwd(const char *path, char *resolvedOut, FwSizeType resolvedSize)
Resolve a path using CWD as the base for relative paths.
A read-only abstract superclass for StringBase.
Status resolvePath(const char *path, const char *baseDir, char *resolvedOut, FwSizeType resolvedSize)
Resolve a path by collapsing . and .. segments.
Combined path length exceeds the output buffer size.
#define FW_ASSERT(...)
Definition: Assert.hpp:14
FwSizeType string_length(const CHAR *source, FwSizeType buffer_size)
get the length of the source string
Definition: StringUtils.cpp:32