#include "fesa/io/abaqus/input_reader.h" #include #include #include #include #include #include #include #include #include #include #include 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(character - 'a' + 'A'); } return character; }); return value; } std::vector SplitFields(std::string_view line) { std::vector 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& 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(byte); hash *= kPrime; } std::ostringstream formatted; formatted << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16) << hash; return formatted.str(); } Result 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::Failure( Status::Failure(FailureCategory::kInput, {std::move(diagnostic)})); } } // namespace Result 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{stream}, std::istreambuf_iterator{}}; 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 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::Success(std::move(parsed)); } } // namespace fesa