Files
FESADev/src/fesa/io/abaqus/input_reader.cpp
T

268 lines
9.6 KiB
C++

#include "fesa/io/abaqus/input_reader.h"
#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>
namespace fesa {
namespace {
std::filesystem::path NormalizedPath(const std::filesystem::path& path) {
std::error_code error;
const auto absolute = std::filesystem::absolute(path, error);
return (error ? path : absolute).lexically_normal();
}
bool IsAsciiWhitespace(char value) noexcept {
return value == ' ' || value == '\t' || value == '\r' || value == '\n' ||
value == '\f' || value == '\v';
}
bool IsAllowedInputCharacter(unsigned char value) noexcept {
return value == '\t' || (value >= 0x20U && value <= 0x7eU);
}
std::string Trim(std::string_view text) {
while (!text.empty() && IsAsciiWhitespace(text.front())) {
text.remove_prefix(1U);
}
while (!text.empty() && IsAsciiWhitespace(text.back())) {
text.remove_suffix(1U);
}
return std::string{text};
}
std::string UppercaseAscii(std::string value) {
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
if (character >= 'a' && character <= 'z') {
return static_cast<char>(character - 'a' + 'A');
}
return character;
});
return value;
}
std::vector<std::string> SplitFields(std::string_view line) {
std::vector<std::string> fields;
std::size_t field_start = 0U;
while (true) {
const std::size_t separator = line.find(',', field_start);
if (separator == std::string_view::npos) {
fields.push_back(Trim(line.substr(field_start)));
break;
}
fields.push_back(Trim(line.substr(field_start, separator - field_start)));
field_start = separator + 1U;
}
return fields;
}
bool IsKeywordContinuation(std::string_view trimmed_line) noexcept {
return !trimmed_line.empty() && trimmed_line.back() == ',';
}
bool AppendKeywordParameters(KeywordBlock& block,
const std::vector<std::string>& fields,
std::size_t first_field,
bool has_continuation) {
if (has_continuation && fields.empty()) {
return false;
}
const std::size_t field_count =
has_continuation ? fields.size() - 1U : fields.size();
if (field_count < first_field) {
return false;
}
for (std::size_t index = first_field; index < field_count; ++index) {
const std::string& field = fields[index];
if (field.empty()) {
return false;
}
const std::size_t equals = field.find('=');
const std::string parameter_name =
Trim(std::string_view{field}.substr(0U, equals));
if (parameter_name.empty()) {
return false;
}
KeywordParameter parameter{UppercaseAscii(parameter_name), std::nullopt};
if (equals != std::string::npos) {
parameter.value = Trim(std::string_view{field}.substr(equals + 1U));
}
block.parameters.push_back(std::move(parameter));
}
return true;
}
/// @brief Computes the stable identity of the exact source bytes.
/// @note This runs before line-ending handling so parser provenance is not
/// affected by text normalization.
std::string ContentIdentity(const std::string& bytes) {
constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL;
constexpr std::uint64_t kPrime = 1099511628211ULL;
std::uint64_t hash = kOffsetBasis;
// Hash the binary input before CRLF handling so provenance follows the
// exact file bytes rather than a normalized text representation.
for (const unsigned char byte : bytes) {
hash ^= static_cast<std::uint64_t>(byte);
hash *= kPrime;
}
std::ostringstream formatted;
formatted << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16)
<< hash;
return formatted.str();
}
Result<ParsedInput> Failure(const std::filesystem::path& source_path,
std::size_t line, std::string code,
std::string keyword, std::string message) {
Diagnostic diagnostic{Severity::kError,
std::move(code),
{source_path, line},
std::move(keyword),
"",
std::move(message)};
return Result<ParsedInput>::Failure(
Status::Failure(FailureCategory::kInput, {std::move(diagnostic)}));
}
} // namespace
Result<ParsedInput> AbaqusInputReader::Read(
const std::filesystem::path& input_path) const {
const auto source_path = NormalizedPath(input_path);
std::ifstream stream{source_path, std::ios::binary};
if (!stream) {
return Failure(source_path, 0U, "input-file-unreadable", "",
"The Abaqus input file could not be opened for reading.");
}
const std::string bytes{std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
if (stream.bad()) {
return Failure(source_path, 0U, "input-file-unreadable", "",
"The Abaqus input file could not be read completely.");
}
struct PhysicalLine {
std::string original_line;
std::string trimmed_line;
std::size_t line_number;
};
std::vector<PhysicalLine> physical_lines;
std::size_t line_start = 0U;
std::size_t line_number = 1U;
while (line_start < bytes.size()) {
const std::size_t newline = bytes.find('\n', line_start);
const std::size_t line_end =
newline == std::string::npos ? bytes.size() : newline;
std::string original_line = bytes.substr(line_start, line_end - line_start);
if (!original_line.empty() && original_line.back() == '\r') {
original_line.pop_back();
}
if (original_line.size() > 256U) {
return Failure(source_path, line_number, "input-line-too-long", "",
"Each physical input line is limited to 256 ASCII bytes.");
}
for (const unsigned char character : original_line) {
if (!IsAllowedInputCharacter(character)) {
return Failure(source_path, line_number, "invalid-input-character", "",
"Input lines must use printable 7-bit ASCII or tabs.");
}
}
physical_lines.push_back(
{original_line, Trim(original_line), line_number});
if (newline == std::string::npos) {
break;
}
line_start = newline + 1U;
++line_number;
}
ParsedInput parsed{source_path, ContentIdentity(bytes), {}};
for (std::size_t line_index = 0U; line_index < physical_lines.size();
++line_index) {
const PhysicalLine& line = physical_lines[line_index];
const std::string& trimmed_line = line.trimmed_line;
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) {
if (trimmed_line.front() == '*') {
const auto fields = SplitFields(trimmed_line);
const std::string keyword_text =
fields.empty() ? std::string{}
: Trim(std::string_view{fields[0]}.substr(1U));
if (keyword_text.empty()) {
return Failure(source_path, line.line_number, "malformed-keyword",
trimmed_line,
"A keyword line requires a non-empty keyword name.");
}
KeywordBlock block{UppercaseAscii(keyword_text),
line.original_line,
{line.original_line},
{},
{},
{source_path, line.line_number}};
bool continuation = IsKeywordContinuation(trimmed_line);
if (!AppendKeywordParameters(block, fields, 1U, continuation)) {
return Failure(source_path, line.line_number, "malformed-keyword",
block.canonical_name,
"A keyword parameter name cannot be empty.");
}
while (continuation) {
++line_index;
if (line_index >= physical_lines.size()) {
return Failure(source_path, line.line_number, "malformed-keyword",
block.canonical_name,
"A keyword continuation requires a following line.");
}
const PhysicalLine& continuation_line = physical_lines[line_index];
if (continuation_line.trimmed_line.empty() ||
continuation_line.trimmed_line.rfind("**", 0U) == 0U ||
continuation_line.trimmed_line.front() == '*') {
return Failure(source_path, continuation_line.line_number,
"malformed-keyword", block.canonical_name,
"A keyword continuation cannot be interrupted.");
}
const auto continuation_fields =
SplitFields(continuation_line.trimmed_line);
continuation =
IsKeywordContinuation(continuation_line.trimmed_line);
if (!AppendKeywordParameters(block, continuation_fields, 0U,
continuation)) {
return Failure(source_path, continuation_line.line_number,
"malformed-keyword", block.canonical_name,
"A keyword parameter name cannot be empty.");
}
block.physical_lines.push_back(continuation_line.original_line);
}
parsed.blocks.push_back(std::move(block));
} else {
if (parsed.blocks.empty()) {
return Failure(source_path, line.line_number, "orphan-data-line", "",
"A data line must follow a keyword line.");
}
parsed.blocks.back().data.push_back(
{SplitFields(line.original_line), {source_path, line.line_number}});
}
}
}
return Result<ParsedInput>::Success(std::move(parsed));
}
} // namespace fesa