268 lines
9.6 KiB
C++
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
|