GCC Code Coverage Report


Directory: ./
File: Svc/FileWorker/FileWorker.cpp
Date: 2026-09-03 22:12:29
Exec Total Coverage
Lines: 0 322 0.0%
Functions: 0 14 0.0%
Branches: 0 245 0.0%

Line Branch Exec Source
1 // ======================================================================
2 // \title FileWorker.cpp
3 // \author racheljt
4 // \brief cpp file for FileWorker component implementation class
5 // ======================================================================
6
7 #include "Svc/FileWorker/FileWorker.hpp"
8
9 namespace Svc {
10
11 // ----------------------------------------------------------------------
12 // Component construction and destruction
13 // ----------------------------------------------------------------------
14
15 FileWorker ::FileWorker(const char* const compName)
16 : FileWorkerComponentBase(compName),
17 m_state(FileWorkerState::FW_STATE_IDLE),
18 m_abort(false),
19 m_chunkSize(BLOCK_SIZE_BYTES) {}
20
21 void FileWorker ::configure(U64 chunkSize) {
22 FW_ASSERT(chunkSize > 0);
23 this->m_chunkSize = chunkSize;
24 }
25
26 FileWorker ::~FileWorker() {}
27
28 // ----------------------------------------------------------------------
29 // Handler implementations for typed input ports
30 // ----------------------------------------------------------------------
31
32 void FileWorker ::cancelIn_handler(FwIndexType portNum) {
33 this->m_abort.store(true, std::memory_order_relaxed);
34 }
35
36 void FileWorker ::readIn_handler(FwIndexType portNum, const Fw::StringBase& path, Fw::Buffer& buffer) {
37 // Validate inputs before processing file
38 if (path.length() == 0) {
39 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("readIn"), Fw::LogStringArg("empty path"));
40 this->readDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
41 return;
42 }
43 if (!buffer.isValid()) {
44 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("readIn"), Fw::LogStringArg("invalid buffer"));
45 this->readDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
46 return;
47 }
48
49 const char* const fileName = path.toChar();
50 FwSizeType fileSize = 0;
51
52 if (this->m_state != FW_STATE_IDLE) {
53 this->log_WARNING_HI_NotInIdle(this->m_state);
54 this->readDoneOut_out(0, FW_STATUS_NOT_IDLE, 0);
55 return;
56 }
57
58 // New read request overrides any leftover abort state
59 this->m_abort.store(false, std::memory_order_relaxed);
60
61 this->m_state = FW_STATE_READING;
62
63 // Check CRC
64 U32 crcFromFile = 0;
65 U32 crcCalculated = 0;
66 Utils::crc_stat_t crcStat = Utils::verify_checksum(fileName, crcFromFile, crcCalculated);
67 if (crcStat != Utils::PASSED_FILE_CRC_CHECK) {
68 this->log_WARNING_HI_CrcFailed(crcStat);
69 this->readDoneOut_out(0, FW_STATUS_FAILED_CRC, 0);
70 this->m_state = FW_STATE_IDLE;
71 return;
72 }
73
74 // Get filesize
75 Os::FileSystem::Status fsStat = Os::FileSystem::getFileSize(fileName, fileSize);
76 if (fsStat != Os::FileSystem::OP_OK) {
77 // Path is ground-controlled and the file may change between the CRC check and here
78 this->log_WARNING_HI_ReadFailedFileSize(fsStat);
79 this->readDoneOut_out(0, FW_STATUS_FAILED_FILE_SIZE, 0);
80 this->m_state = FW_STATE_IDLE;
81 return;
82 }
83
84 // Start reading
85 FileWorkerStatus workerStat = this->readBufferFromFile(buffer, fileName);
86
87 // Report 0 bytes on a failed or aborted read so readDoneOut does not imply success.
88 this->readDoneOut_out(0, workerStat, (workerStat == FW_STATUS_DONE_READ) ? buffer.getSize() : 0);
89 this->m_state = FW_STATE_IDLE;
90 }
91
92 void FileWorker ::verifyIn_handler(FwIndexType portNum, const Fw::StringBase& path, U32 crc) {
93 // Validate inputs before processing file
94 if (path.length() == 0) {
95 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("verifyIn"), Fw::LogStringArg("empty path"));
96 this->verifyDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
97 return;
98 }
99
100 const char* const fileName = path.toChar();
101 FwSizeType fileSize = 0;
102 FileWorkerStatus workerStat = FW_STATUS_DONE;
103
104 U32 crcFromFile = 0;
105 U32 crcCalculated = 0;
106 Utils::crc_stat_t crcStat = Utils::verify_checksum(fileName, crcFromFile, crcCalculated);
107
108 if (crcStat != Utils::PASSED_FILE_CRC_CHECK) {
109 this->log_WARNING_HI_CrcFailed(crcStat);
110 workerStat = FW_STATUS_FAILED_CRC;
111 }
112
113 if (crc != crcCalculated) {
114 workerStat = FW_STATUS_FAILED_CRC;
115 this->log_WARNING_LO_CrcVerificationError(crc, crcCalculated);
116 }
117
118 // Get filesize
119 Os::FileSystem::Status fsStat = Os::FileSystem::getFileSize(fileName, fileSize);
120 if (fsStat != Os::FileSystem::OP_OK) {
121 this->log_WARNING_HI_ReadFailedFileSize(fsStat);
122 workerStat = FW_STATUS_FAILED_FILE_SIZE;
123 }
124
125 this->verifyDoneOut_out(0, workerStat, fileSize);
126 }
127
128 void FileWorker ::writeIn_handler(FwIndexType portNum,
129 const Fw::StringBase& path,
130 Fw::Buffer& buffer,
131 FwSizeType offsetBytes,
132 bool append) {
133 // Validate inputs before processing file
134 if (path.length() == 0) {
135 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("writeIn"), Fw::LogStringArg("empty path"));
136 this->writeDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
137 return;
138 }
139 if (!buffer.isValid()) {
140 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("writeIn"), Fw::LogStringArg("invalid buffer"));
141 this->writeDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
142 return;
143 }
144 if (offsetBytes > buffer.getSize()) {
145 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("writeIn"), Fw::LogStringArg("invalid offset"));
146 this->writeDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
147 return;
148 }
149
150 char fileName[FileNameStringSize];
151
152 // Make sure we are in IDLE state before proceeding
153 if (this->m_state != FW_STATE_IDLE) {
154 this->log_WARNING_HI_NotInIdle(this->m_state);
155 this->writeDoneOut_out(0, FW_STATUS_NOT_IDLE, 0);
156 return;
157 }
158
159 this->m_state = FW_STATE_WRITING;
160
161 // New write request overrides any leftover abort state
162 this->m_abort.store(false, std::memory_order_relaxed);
163
164 // Save file name
165 // NB: may count null terminator due to FPRIME/fprime-sw#57, but should still be less than FileNameStringSize in any
166 // case
167 FwSizeType length = Fw::StringUtils::string_length(path.toChar(), FileNameStringSize);
168 if (length >= FileNameStringSize || length >= sizeof(fileName)) {
169 // Path length is ground-controlled, so an oversized path is invalid input, not a coding error.
170 this->log_WARNING_HI_InvalidInput(Fw::LogStringArg("writeIn"), Fw::LogStringArg("path too long"));
171 this->writeDoneOut_out(0, FW_STATUS_INVALID_INPUT, 0);
172 this->m_state = FW_STATE_IDLE;
173 return;
174 }
175
176 (void)Fw::StringUtils::string_copy(fileName, path.toChar(), sizeof(fileName));
177 fileName[sizeof(fileName) - 1] = 0; // guarantee termination
178
179 // Write
180 const bool isWrite = this->writeBufferToFile(buffer, fileName, offsetBytes, append);
181 if (isWrite) {
182 this->writeBufferHashToFile(buffer, fileName, offsetBytes, append);
183 }
184
185 // Report the actual outcome of the write. A failed writeBufferToFile (open
186 // failure, permission denied, disk full, partial write) must not be reported
187 // to ground as a successful FW_STATUS_DONE_WRITE.
188 const FileWorkerStatus writeStatus = isWrite ? FW_STATUS_DONE_WRITE : FW_STATUS_FAILED_TO_WRITE;
189 // Report bytes actually written, which excludes the skipped offset. Reporting the full
190 // buffer size over-reports by offsetBytes, and reports a whole buffer for a zero-length
191 // write. offsetBytes <= buffer.getSize() is checked above, so this cannot underflow.
192 const FwSizeType writtenBytes = buffer.getSize() - offsetBytes;
193 this->writeDoneOut_out(0, writeStatus, isWrite ? writtenBytes : 0);
194 this->m_state = FW_STATE_IDLE;
195 return;
196 }
197
198 // ----------------------------------------------------------------------
199 // Helper functions
200 // ----------------------------------------------------------------------
201
202 Svc ::FileWorkerStatus FileWorker ::readBufferFromFile(Fw::Buffer& buffer, const char* const fileName) {
203 FW_ASSERT(buffer.getData() != nullptr);
204 FW_ASSERT(fileName != nullptr);
205
206 Fw::LogStringArg fileNameStr(fileName);
207 Os::File file;
208
209 // Open file
210 Os::File::Status fileStat = file.open(fileName, Os::File::OPEN_READ);
211 if (fileStat != Os::File::OP_OK) {
212 this->log_WARNING_HI_OpenFileError(fileNameStr, fileStat);
213 return FW_STATUS_FAILED_TO_OPEN;
214 }
215
216 // Get buffer data and size
217 FwSizeType readSize = buffer.getSize();
218
219 // Read file
220 this->log_ACTIVITY_LO_ReadBegin(readSize, fileNameStr);
221 const FileWorkerReadStatus readStat = this->readFile(buffer, readSize, file, fileNameStr);
222
223 this->log_ACTIVITY_LO_ReadCompleted(readSize, fileNameStr);
224 file.close();
225
226 // Only a completed read is DONE_READ; error, abort, or timeout reports FAILED_TO_READ.
227 return (readStat == FileWorkerReadStatus::FW_READ_DONE) ? FileWorkerStatus::FW_STATUS_DONE_READ
228 : FileWorkerStatus::FW_STATUS_FAILED_TO_READ;
229 }
230
231 Svc ::FileWorkerReadStatus FileWorker ::readFile(Fw::Buffer& buffer,
232 FwSizeType size,
233 Os::File& file,
234 const Fw::LogStringArg& fileNameStr) {
235 FW_ASSERT(buffer.getData() != nullptr);
236 FW_ASSERT(size > 0);
237 FW_ASSERT(fileNameStr != nullptr);
238
239 FwSizeType bytesRead = 0;
240 FwSizeType numChunks = 0;
241 U64 timeout = 0;
242
243 if (!file.isOpen()) {
244 return FileWorkerReadStatus::FW_READ_ERROR;
245 }
246
247 FileWorkerReadStatus readStat = this->readFileBytes(buffer, size, file, bytesRead);
248
249 switch (readStat) {
250 case FW_READ_ERROR:
251 // Some read error
252 this->log_WARNING_HI_ReadError(bytesRead, size, fileNameStr);
253 break;
254
255 case FW_READ_DONE:
256 break;
257
258 case FW_READ_ABORT:
259 // Abort command was sent
260 this->log_WARNING_LO_ReadAborted(bytesRead, size, fileNameStr);
261 break;
262
263 case FW_READ_TIMEOUT:
264 // Determine true timeout
265 FW_ASSERT(this->m_chunkSize > 0);
266 numChunks = (size / this->m_chunkSize);
267 if (size % this->m_chunkSize > 0) {
268 numChunks += 1;
269 }
270 timeout = numChunks * TIMEOUT_MS;
271 this->log_WARNING_HI_ReadTimeout(bytesRead, size, fileNameStr, timeout);
272 break;
273
274 case FW_READ_UNKNOWN:
275 // The read loop ran out of iterations: a read larger than
276 // MAX_LOOP_ITERATIONS * BLOCK_SIZE_BYTES cannot complete
277 this->log_WARNING_HI_ReadError(bytesRead, size, fileNameStr);
278 break;
279
280 default:
281 FW_ASSERT(false, static_cast<FwAssertArgType>(readStat));
282 break;
283 }
284
285 return readStat;
286 }
287
288 Svc ::FileWorkerReadStatus FileWorker ::readFileBytes(Fw::Buffer& buffer,
289 FwSizeType size,
290 Os::File& file,
291 FwSizeType& bytesRead) {
292 FW_ASSERT(buffer.getData() != nullptr);
293 FW_ASSERT(size > 0);
294
295 // Determine true timeout
296 FW_ASSERT(this->m_chunkSize > 0);
297 FwSizeType numChunks = (size / this->m_chunkSize);
298 if (size % this->m_chunkSize > 0) {
299 numChunks += 1;
300 }
301 U64 timeout = numChunks * TIMEOUT_MS;
302
303 // Read loop
304 bytesRead = 0;
305 Fw::Time start = this->getTime();
306
307 for (FwSizeType i = 0; i < numChunks; i++) {
308 FwSizeType readAmt = FW_MIN(size - bytesRead, this->m_chunkSize);
309 FwSizeType readAmtActual = readAmt;
310 Os::File::Status ret = file.read(buffer.getData() + bytesRead, readAmtActual);
311
312 if (Os::File::OP_OK != ret || readAmt != readAmtActual) {
313 // Count the bytes actually transferred so ReadError telemetry reports
314 // the true amount. A short read stays an error on purpose: FileWorker
315 // reads a fixed, caller-specified size and must not silently accept a
316 // file shorter than expected (e.g. truncated mid-read).
317 bytesRead += readAmtActual;
318 return FileWorkerReadStatus::FW_READ_ERROR;
319 }
320
321 bool currAbort = this->m_abort.load(std::memory_order_relaxed);
322 if (currAbort) {
323 // Abort command was sent
324 return FileWorkerReadStatus::FW_READ_ABORT;
325 }
326
327 if (timeout > 0) {
328 // Only check timeout if > 0
329 Fw::Time now = this->getTime();
330 Fw::Time diff = Fw::Time::sub(now, start);
331 U64 elapsed = (diff.getSeconds() * 1000000) + diff.getUSeconds();
332 if (elapsed >= timeout) {
333 return FileWorkerReadStatus::FW_READ_TIMEOUT;
334 }
335 }
336
337 bytesRead += readAmt;
338 if (bytesRead >= size) {
339 // Finished, break out
340 return FileWorkerReadStatus::FW_READ_DONE;
341 }
342 }
343
344 return FileWorkerReadStatus::FW_READ_UNKNOWN;
345 }
346
347 bool FileWorker ::getHash(const char* const hashFileName,
348 Utils::Hash& hash,
349 Utils::HashBuffer& hashBuffer,
350 const U8* const data,
351 const FwSizeType size) {
352 FW_ASSERT(hashFileName != nullptr);
353 FW_ASSERT(data != nullptr);
354 FW_ASSERT(size > 0);
355
356 // Open file
357 Os::File file;
358 Os::File::Status stat = file.open(hashFileName, Os::File::OPEN_READ);
359
360 // Read value if it exists
361 if (stat == Os::File::OP_OK) {
362 HASH_HANDLE_TYPE hashValue;
363 FwSizeType hashSize = sizeof(hashValue);
364 U8* hashValuePtr = reinterpret_cast<U8*>(&hashValue);
365 FW_ASSERT(hashValuePtr != nullptr);
366
367 Os::File::Status readStat = file.read(hashValuePtr, hashSize);
368 if (readStat != Os::File::OP_OK) {
369 Fw::LogStringArg s(hashFileName);
370 this->log_WARNING_HI_WriteValidationReadError(s, readStat);
371 return false;
372 }
373 Utils::HashBuffer tmp(hashValuePtr, hashSize);
374 hash.setHashValue(tmp);
375 hash.update(data, size);
376 hash.finalize(hashBuffer);
377
378 } else if (stat == Os::File::DOESNT_EXIST) {
379 hash.hash(data, size, hashBuffer);
380
381 } else {
382 Fw::LogStringArg s(hashFileName);
383 this->log_WARNING_HI_WriteValidationOpenError(s, stat);
384 return false;
385 }
386
387 return true;
388 }
389
390 bool FileWorker ::writeBufferToFile(Fw::Buffer& buffer, const char* fileName, FwSizeType offset, bool append) {
391 FW_ASSERT(buffer.getData() != nullptr);
392 FW_ASSERT(fileName != nullptr);
393
394 Fw::LogStringArg logStringArg(fileName);
395
396 // Get buffer data and size, then apply offset
397 FwSizeType size = buffer.getSize();
398 U8* const data = reinterpret_cast<U8*>(buffer.getData());
399 FW_ASSERT(data != nullptr);
400 FW_ASSERT(offset <= size);
401 size -= offset;
402
403 // A zero-length write (offset == buffer size, a valid "nothing left to write" boundary
404 // permitted by writeIn_handler's offset check) is a successful no-op. Return before
405 // opening the file: this avoids reaching FW_ASSERT(size > 0) in writeToFile(), and avoids
406 // creating an empty file for a request that writes nothing, since both OPEN_WRITE and
407 // OPEN_APPEND pass O_CREAT. An existing file's contents are not at risk either way here:
408 // OPEN_WRITE overwrites in place and does not truncate; only OPEN_CREATE sets O_TRUNC.
409 if (size == 0) {
410 this->log_ACTIVITY_LO_WriteCompleted(size, logStringArg);
411 return true;
412 }
413
414 U8* const dataFromOffset = reinterpret_cast<U8*>(data + offset);
415 FW_ASSERT(dataFromOffset != nullptr);
416
417 Os::File file;
418 Os::File::Status stat = Os::File::OP_OK;
419
420 // Open file
421 if (!append) {
422 stat = file.open(fileName, Os::File::Mode::OPEN_WRITE);
423 } else {
424 stat = file.open(fileName, Os::File::Mode::OPEN_APPEND);
425 }
426
427 if (stat != Os::File::OP_OK) {
428 this->log_WARNING_HI_OpenFileError(logStringArg, stat);
429 return false;
430 }
431
432 // Write file
433 this->log_ACTIVITY_LO_WriteBegin(size, logStringArg);
434 FwSizeType writtenSize = this->writeToFile(dataFromOffset, size, file, fileName);
435
436 // Check written size
437 if (writtenSize != size) {
438 return false;
439 }
440
441 this->log_ACTIVITY_LO_WriteCompleted(size, logStringArg);
442 return true;
443 }
444
445 void FileWorker ::writeBufferHashToFile(Fw::Buffer& buffer, const char* fileName, FwSizeType offset, bool append) {
446 FW_ASSERT(buffer.getData() != nullptr);
447 FW_ASSERT(fileName != nullptr);
448
449 // Construct hash file name
450 const char* ext = Utils::Hash::getFileExtensionString();
451 FW_ASSERT(ext != nullptr);
452 char hashFileName[FileNameStringSize];
453 Fw::FormatStatus status = Fw::stringFormat(hashFileName, sizeof(hashFileName), "%s%s", fileName, ext);
454 FW_ASSERT(status == Fw::FormatStatus::SUCCESS);
455
456 // Compute hash
457 Utils::HashBuffer hashBuffer;
458 FwSizeType size = buffer.getSize();
459 U8* const data = reinterpret_cast<U8*>(buffer.getData());
460 FW_ASSERT(data != nullptr);
461
462 // Apply offset
463 FW_ASSERT(offset <= size);
464 size -= offset; // checked by assert
465
466 // A zero-length write changed no file contents, so the hash must not change either. Skip
467 // generation entirely: on the append path this would otherwise trip FW_ASSERT(size > 0) in
468 // getHash(), and on the non-append path it would silently overwrite a valid hash file with
469 // the hash of zero bytes.
470 if (size == 0) {
471 return;
472 }
473
474 U8* const dataFromOffset = reinterpret_cast<U8*>(data + offset);
475 FW_ASSERT(dataFromOffset != nullptr);
476
477 Utils::Hash hash;
478 if (!append) {
479 hash.hash(dataFromOffset, size, hashBuffer);
480
481 } else {
482 bool isHash = this->getHash(hashFileName, hash, hashBuffer, dataFromOffset, size);
483 if (!isHash) {
484 return;
485 }
486 }
487
488 // Open file
489 Os::File file;
490 Os::File::Status stat = file.open(hashFileName, Os::File::Mode::OPEN_WRITE);
491 if (stat != Os::File::OP_OK) {
492 Fw::LogStringArg logStringArg(hashFileName);
493 this->log_WARNING_HI_OpenFileError(logStringArg, stat);
494 return;
495 }
496
497 // Write hash
498 FwSizeType writtenSize = this->writeToFile(hashBuffer.getBuffAddr(), hashBuffer.getSize(), file, hashFileName);
499
500 // Check written size
501 FwSizeType hashSize = hashBuffer.getSize();
502 if (writtenSize != hashSize) {
503 Fw::LogStringArg logStringArg(hashFileName);
504 this->log_WARNING_LO_WriteValidationError(logStringArg, writtenSize, hashSize);
505 return;
506 }
507
508 return;
509 }
510
511 FwSizeType FileWorker ::writeToFile(const U8* data, FwSizeType size, Os::File& file, const char* fileName) {
512 FW_ASSERT(data != nullptr);
513 FW_ASSERT(size > 0);
514 FW_ASSERT(file.isOpen());
515 FW_ASSERT(fileName != nullptr);
516
517 // Determine true timeout
518 FW_ASSERT(this->m_chunkSize > 0);
519 FwSizeType numChunks = (size / this->m_chunkSize);
520 if (size % this->m_chunkSize > 0) {
521 numChunks += 1;
522 }
523 U64 timeout = numChunks * TIMEOUT_MS;
524
525 // Write loop: legal short writes make progress but consume an iteration, so
526 // allow extra iterations beyond the chunk count before giving up
527 const FwSizeType maxIterations = numChunks + MAX_LOOP_ITERATIONS;
528 FwSizeType bytesWritten = 0;
529 Fw::Time start = this->getTime();
530 for (FwSizeType i = 0; (i < maxIterations) && (bytesWritten < size); i++) {
531 FwSizeType writeAmt = FW_MIN(size - bytesWritten, this->m_chunkSize);
532 Os::File::Status ret = file.write(data + bytesWritten, writeAmt);
533
534 if (Os::File::OP_OK != ret || writeAmt == 0) {
535 Fw::LogStringArg logStringArg(fileName);
536 this->log_WARNING_HI_WriteFileError(bytesWritten, size, logStringArg, ret);
537 break;
538 }
539
540 bool currAbort = this->m_abort.load(std::memory_order_relaxed);
541 if (currAbort) {
542 // Abort command was sent
543 Fw::LogStringArg logStringArg(fileName);
544 this->log_WARNING_LO_WriteAborted(bytesWritten, size, logStringArg);
545 break;
546 }
547
548 if (timeout > 0) {
549 // Only check timeout if > 0
550 Fw::Time now = this->getTime();
551 Fw::Time diff = Fw::Time::sub(now, start);
552 U64 elapsed = (diff.getSeconds() * 1000000) + diff.getUSeconds();
553
554 if (elapsed >= timeout) {
555 Fw::LogStringArg logStringArg(fileName);
556 this->log_WARNING_HI_WriteTimeout(bytesWritten, size, logStringArg, timeout);
557 break;
558 }
559 }
560
561 bytesWritten += writeAmt;
562 }
563
564 return bytesWritten;
565 }
566
567 } // namespace Svc
568