diff --git a/src/App/MappedName.h b/src/App/MappedName.h index 77e51dabf5..0d16018a5b 100644 --- a/src/App/MappedName.h +++ b/src/App/MappedName.h @@ -1,25 +1,26 @@ -/**************************************************************************** - * Copyright (c) 2020 Zheng, Lei (realthunder) * - * * - * This file is part of the FreeCAD CAx development system. * - * * - * This library is free software; you can redistribute it and/or * - * modify it under the terms of the GNU Library General Public * - * License as published by the Free Software Foundation; either * - * version 2 of the License, or (at your option) any later version. * - * * - * This library is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU Library General Public License for more details. * - * * - * You should have received a copy of the GNU Library General Public * - * License along with this library; see the file COPYING.LIB. If not, * - * write to the Free Software Foundation, Inc., 59 Temple Place, * - * Suite 330, Boston, MA 02111-1307, USA * - * * - ****************************************************************************/ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * Copyright (c) 2022 Zheng, Lei (realthunder) * + * Copyright (c) 2023 FreeCAD Project Association * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ #ifndef APP_MAPPED_NAME_H #define APP_MAPPED_NAME_H @@ -41,6 +42,11 @@ namespace Data // NOLINTBEGIN(cppcoreguidelines-pro-bounds-pointer-arithmetic) +/// The MappedName class maintains a two-part name: the first part ("data") is considered immutable +/// once created, while the second part ("postfix") can be modified/appended to by later operations. +/// It uses shared data when possible (see the fromRawData() members). Despite storing data and +/// postfix separately, they can be accessed via calls to size(), operator[], etc. as though they +/// were a single array. class AppExport MappedName { public: @@ -104,19 +110,25 @@ public: /// /// \param other The MappedName to copy /// \param startPosition an integer offset to start the copy from - /// \param size the number of bytes to copy. If not specified + /// \param size the number of bytes to copy. + /// \see append() for details about how the copy behaves for various sizes and start positions MappedName(const MappedName& other, int startPosition, int size = -1) : raw(false) { append(other, startPosition, size); } + /// Copy constructor with additional postfix + /// + /// \param other The mapped name to copy. Its data and postfix become the new MappedName's data + /// \param postfix The postfix for the new MappedName MappedName(const MappedName& other, const char* postfix) : data(other.data + other.postfix), postfix(postfix), raw(false) {} + /// Move constructor MappedName(MappedName&& other) noexcept : data(std::move(other.data)), postfix(std::move(other.postfix)), @@ -125,6 +137,12 @@ public: ~MappedName() = default; + /// Construct a MappedName from raw character data (including null characters, if size is + /// provided). No copy is made: the data is used in place. + /// + /// \param name The raw data to use. + /// \param size The number of bytes to access. If omitted, name must be null-terminated. + /// \return a new MappedName with name as its data. static MappedName fromRawData(const char* name, int size = -1) { MappedName res; @@ -136,11 +154,25 @@ public: return res; } + /// Construct a MappedName from QByteArray data (including any embedded null characters). + /// + /// \param data The original data. No copy is made, the data is shared with the other instance. + /// \return a new MappedName with data as its data. static MappedName fromRawData(const QByteArray& data) { return fromRawData(data.constData(), data.size()); } + /// Construct a MappedName from another MappedName + /// + /// \param other The MappedName to copy from. The data is usually not copied, but in some + /// cases a partial copy may be made to support a slice that extends across other's data into + /// its postfix. + /// \param startPosition The position to start the reference at. + /// \param size The number of bytes to access. If omitted, continues from startPosition + /// to the end of available data (including postfix). + /// \return a new MappedName sharing (possibly a subset of) data with other. + /// \see append() for details about how the copy behaves for various sizes and start positions static MappedName fromRawData(const MappedName& other, int startPosition, int size = -1) { if (startPosition < 0) { @@ -178,14 +210,17 @@ public: return res; } + /// Share data with another MappedName MappedName& operator=(const MappedName& other) = default; + /// Create a new MappedName from a std::string: the string's data is copied. MappedName& operator=(const std::string& other) { *this = MappedName(other); return *this; } + /// Create a new MappedName from a const char *. The character data is copied. MappedName& operator=(const char* other) { *this = MappedName(other); @@ -193,6 +228,7 @@ public: } + /// Move-construct a MappedName MappedName& operator=(MappedName&& other) noexcept { this->data = std::move(other.data); @@ -201,6 +237,8 @@ public: return *this; } + /// Write to a stream as the name with postfix directly appended to it. Note that there is no + /// special handling for null or non-ASCII characters, they are simply written to the stream. friend std::ostream& operator<<(std::ostream& stream, const MappedName& mappedName) { stream.write(mappedName.data.constData(), mappedName.data.size()); @@ -208,6 +246,8 @@ public: return stream; } + /// Two MappedNames are equal if the concatenation of their data and postfix is equal. The + /// individual data and postfix may NOT be equal in this case. bool operator==(const MappedName& other) const { if (this->size() != other.size()) { @@ -243,6 +283,8 @@ public: return !(this->operator==(other)); } + /// Returns a new MappedName whose data is the LHS argument's data and whose postfix is the LHS + /// argument's postfix with the RHS argument's data and postfix appended to it. MappedName operator+(const MappedName& other) const { MappedName res(*this); @@ -250,6 +292,8 @@ public: return res; } + /// Returns a new MappedName whose data is the LHS argument's data and whose postfix is the LHS + /// argument's postfix with the RHS argument appended to it. The character data is copied. MappedName operator+(const char* other) const { MappedName res(*this); @@ -257,6 +301,8 @@ public: return res; } + /// Returns a new MappedName whose data is the LHS argument's data and whose postfix is the LHS + /// argument's postfix with the RHS argument appended to it. The character data is copied. MappedName operator+(const std::string& other) const { MappedName res(*this); @@ -264,6 +310,8 @@ public: return res; } + /// Returns a new MappedName whose data is the LHS argument's data and whose postfix is the LHS + /// argument's postfix with the RHS argument appended to it. MappedName operator+(const QByteArray& other) const { MappedName res(*this); @@ -271,6 +319,8 @@ public: return res; } + /// Appends other to this instance's postfix. other must be a null-terminated C string. The + /// character data from the string is copied. MappedName& operator+=(const char* other) { if (other && (other[0] != 0)) { @@ -279,6 +329,7 @@ public: return *this; } + /// Appends other to this instance's postfix. The character data from the string is copied. MappedName& operator+=(const std::string& other) { if (!other.empty()) { @@ -288,18 +339,29 @@ public: return *this; } + /// Appends other to this instance's postfix. The data may be either copied or shared, depending + /// on whether this->postfix is empty (in which case the data is shared) or non-empty (in which + /// case it is copied). MappedName& operator+=(const QByteArray& other) { this->postfix += other; return *this; } + /// Appends other to this instance's postfix, unless this is empty, in which case this acts + /// like operator=, and makes this instance's data equal to other's data, and this instance's + /// postfix equal to the other instance's postfix. MappedName& operator+=(const MappedName& other) { append(other); return *this; } + /// Add dataToAppend to this MappedName. If the current name is empty, this becomes the new + /// data element. If this MappedName already has data, then the data is appended to the postfix. + /// + /// \param dataToAppend The data to add. A deep copy is made. + /// \param size The number of bytes to copy. If omitted, dataToAppend must be null-terminated. void append(const char* dataToAppend, int size = -1) { // FIXME raw not assigned? @@ -316,6 +378,20 @@ public: } } + /// Treating both this and other as single continuous byte arrays, append other to this. If this + /// is empty, then other's data is shared with this instance's data beginning at startPosition. + /// If this is *not* empty, then all data is appended to the postfix. If the copy crosses the + /// boundary between other's data and its postfix, then if this instance was empty, the new + /// data stops where other's data stops, and the remainder of the copy is placed in the suffix. + /// Otherwise the copy simply continues as though there was no distinction between other's + /// data and suffix. + /// + /// \param other The MappedName to obtain the data from. The data is shared when possible, + /// depending on the details of startPosition, size, and this->empty(). + /// \param startPosition The byte to start the copy at. Must be a positive non-zero integer less + /// than the length of other's combined data + postfix. + /// \param size The number of bytes to copy. Must not overrun the end of other's combined data + /// storage when taking startPosition into consideration. void append(const MappedName& other, int startPosition = 0, int size = -1) { // enforce 0 <= startPosition <= other.size @@ -377,13 +453,32 @@ public: } } - std::string toString(int startPosition, int len = -1) const + /// Create a std::string from this instance, starting at startPosition, and extending len bytes. + /// + /// \param startPosition The offset into the data + /// \param len The number of bytes to output + /// \return A new std::string containing the bytes copied from this instance's data and postfix + /// (depending on startPosition and len). + /// \note No effort is made to ensure that these are valid ASCII characters, and it is possible + /// the data includes embedded null characters, non-ASCII data, etc. + std::string toString(int startPosition = 0, int len = -1) const { std::string res; - return toString(res, startPosition, len); + return appendToBuffer(res, startPosition, len); } - const char* toString(std::string& buffer, int startPosition = 0, int len = -1) const + /// Given a (possibly non-empty) std::string buffer, append this instance to it, starting at a + /// specified position, and continuing for a specified number of bytes. + /// + /// \param buffer The string buffer to append to. + /// \param startPosition The position in this instance's data/postfix to start at (defaults to + /// zero). Must be less than the total length of the data plus the postfix. + /// \param len The number of bytes to append. If omitted, defaults to appending all available + /// data starting at startPosition. + /// \return A pointer to the beginning of the appended data within buffer. + /// \note No effort is made to ensure that these are valid ASCII characters, and it is possible + /// the data includes embedded null characters, non-ASCII data, etc. + const char* appendToBuffer(std::string& buffer, int startPosition = 0, int len = -1) const { std::size_t offset = buffer.size(); int count = this->size(); @@ -429,6 +524,16 @@ public: return this->data.constData() + offset; } + /// Get access to raw byte data. When possible, data is shared between this instance and the + /// returned QByteArray. If the combination of offset and size results in data that crosses the + /// boundary between this->data and this->postfix, the data must be copied in order to provide + /// access as a continuous array of bytes. + /// + /// \param offset The start position of the raw data access. + /// \param size The number of bytes to access. If omitted, the resulting QByteArray includes + /// everything starting from offset to the end, including any postfix data. + /// \return A new QByteArray that shares data with this instance if possible, or is a new copy + /// if required by offset and size. QByteArray toRawBytes(int offset = 0, int size = -1) const { if (offset < 0) { @@ -453,16 +558,19 @@ public: return res; } + /// Direct access to the stored QByteArray of data. A copy is never made. const QByteArray& dataBytes() const { return this->data; } + /// Direct access to the stored QByteArray of postfix. A copy is never made. const QByteArray& postfixBytes() const { return this->postfix; } + /// Convenience function providing access to the pointer to the beginning of the postfix data. const char* constPostfix() const { return this->postfix.constData(); @@ -470,6 +578,10 @@ public: // No constData() because 'data' is allowed to contain raw data, which may not end with 0. + /// Provide access to the content of this instance. If either postfix or data is empty, no copy + /// is made and the original QByteArray is returned, sharing data with this instance. If this + /// instance contains both data and postfix, a new QByteArray is created and stores a copy of + /// the data and postfix concatenated together. QByteArray toBytes() const { if (this->postfix.isEmpty()) { @@ -481,6 +593,13 @@ public: return this->data + this->postfix; } + /// Create an IndexedName from the data portion of this MappedName. If this data has a postfix, + /// the function returns an empty IndexedName. The function will fail if this->data contains + /// anything other than the ASCII letter a-z, A-Z, and the underscore, with an optional integer + /// suffix, returning an empty IndexedName (e.g. an IndexedName that evaluates to boolean + /// false and isNull() == true). + /// + /// \return a new IndexedName that shares its data with this instance's data member. IndexedName toIndexedName() const { if (this->postfix.isEmpty()) { @@ -489,22 +608,33 @@ public: return IndexedName(); } + /// Create and return a string version of this MappedName prefixed by the ComplexGeoData element + /// map prefix, if this MappedName cannot be converted to an indexed name. std::string toPrefixedString() const { std::string res; - toPrefixedString(res); + appendToBufferWithPrefix(res); return res; } - const char* toPrefixedString(std::string& buf) const + /// Append this MappedName to a provided string buffer, including the ComplexGeoData element + /// map prefix if the MappedName cannot be converted to an IndexedName. + /// + /// \param buf A (possibly non-empty) string to append this MappedName to. + /// \return A pointer to the beginning of the buffer. + const char* appendToBufferWithPrefix(std::string& buf) const { if (!toIndexedName()) { buf += ComplexGeoData::elementMapPrefix(); } - toString(buf); + appendToBuffer(buf); return buf.c_str(); } + /// Equivalent to C++20 operator<=>. Performs byte-by-byte comparison of this and other, + /// starting at the first byte and continuing through both data and postfix, ignoring which is + /// which. If the combined data and postfix members are of unequal size but start with the same + /// data, the shorter array is considered "less than" the longer. int compare(const MappedName& other) const { int thisSize = this->size(); @@ -528,11 +658,14 @@ public: return 0; } + /// \see compare() bool operator<(const MappedName& other) const { return compare(other) < 0; } + /// Treat this MappedName as a single continuous array of bytes, beginning with data and + /// continuing through postfix. No bounds checking is performed when compiled in release mode. char operator[](int index) const { // FIXME overflow underflow checks? @@ -542,21 +675,29 @@ public: return this->data[index]; } + /// Treat this MappedName as a single continuous array of bytes, returning the combined size + /// of the data and postfix. int size() const { return this->data.size() + this->postfix.size(); } + /// Treat this MappedName as a single continuous array of bytes, returning true only if both + /// data and prefix are empty. bool empty() const { return this->data.isEmpty() && this->postfix.isEmpty(); } + /// Returns true if this is shared data, or false if a unique copy has been made. bool isRaw() const { return this->raw; } + /// If this is shared data, a new unshared copy is made and returned. If it is already unshared + /// no new copy is made, a new instance is returned that shares is data with the current + /// instance. MappedName copy() const { if (!this->raw) { @@ -568,13 +709,17 @@ public: return res; } + /// Ensure that this data is unshared, making a copy if necessary. void compact() const; + /// Boolean conversion is the inverse of empty(), returning true if there is data in either the + /// data or postfix, and false if there is nothing in either. explicit operator bool() const { return !empty(); } + /// Reset this instance, clearing anything in data and postfix. void clear() { this->data.clear(); @@ -582,6 +727,12 @@ public: this->raw = false; } + /// Find a string of characters in this MappedName. The bytes must occur either entirely in the + /// data, or entirely in the postfix: a string that overlaps the two will not be found. + /// + /// \param searchTarget A null-terminated C string to search for. + /// \param startPosition A byte offset to start the search at. + /// \return The position of the target in this instance, or -1 if the target is not found. int find(const char* searchTarget, int startPosition = 0) const { if (!searchTarget) { @@ -607,11 +758,25 @@ public: return res + this->data.size(); } + /// Find a string of characters in this MappedName. The bytes must occur either entirely in the + /// data, or entirely in the postfix: a string that overlaps the two will not be found. + /// + /// \param searchTarget A string to search for. + /// \param startPosition A byte offset to start the search at. + /// \return The position of the target in this instance, or -1 if the target is not found. int find(const std::string& searchTarget, int startPosition = 0) const { return find(searchTarget.c_str(), startPosition); } + /// Find a string of characters in this MappedName, starting at the back of postfix and + /// proceeding in reverse through the data. The bytes must occur either entirely in the + /// data, or entirely in the postfix: a string that overlaps the two will not be found. + /// + /// \param searchTarget A null-terminated C string to search for. + /// \param startPosition A byte offset to start the search at. Negative numbers are supported + /// and count back from the end of the concatenated data (as in QByteArray::lastIndexOf()). + /// \return The position of the target in this instance, or -1 if the target is not found. int rfind(const char* searchTarget, int startPosition = -1) const { if (!searchTarget) { @@ -631,11 +796,22 @@ public: return this->data.lastIndexOf(searchTarget, startPosition); } + /// Find a string in this MappedName, starting at the back of postfix and proceeding in reverse + /// through the data. The bytes must occur either entirely in the data, or entirely in the + /// postfix: a string that overlaps the two will not be found. + /// + /// \param searchTarget A null-terminated C string to search for. + /// \param startPosition A byte offset to start the search at. Negative numbers are supported + /// and count back from the end of the concatenated data (as in QByteArray::lastIndexOf()). + /// \return The position of the target in this instance, or -1 if the target is not found. int rfind(const std::string& searchTarget, int startPosition = -1) const { return rfind(searchTarget.c_str(), startPosition); } + /// Returns true if this MappedName ends with the search target. If there is a postfix, only the + /// postfix is considered. If not, then only the data is considered. A search string that + /// overlaps the two will not be found. bool endsWith(const char* searchTarget) const { if (!searchTarget) { @@ -647,11 +823,21 @@ public: return this->data.endsWith(searchTarget); } + /// Returns true if this MappedName ends with the search target. If there is a postfix, only the + /// postfix is considered. If not, then only the data is considered. A search string that + /// overlaps the two will not be found. bool endsWith(const std::string& searchTarget) const { return endsWith(searchTarget.c_str()); } + /// Returns true if this MappedName starts with the search target. If there is a postfix, only + /// the postfix is considered. If not, then only the data is considered. A search string that + /// overlaps the two will not be found. + /// + /// \param searchTarget An array of bytes to match + /// \param offset An offset to perform the match at + /// \return True if this MappedName begins with the target bytes bool startsWith(const QByteArray& searchTarget, int offset = 0) const { if (searchTarget.size() > size() - offset) { @@ -667,6 +853,13 @@ public: return this->postfix.startsWith(searchTarget); } + /// Returns true if this MappedName starts with the search target. If there is a postfix, only + /// the postfix is considered. If not, then only the data is considered. A search string that + /// overlaps the two will not be found. + /// + /// \param searchTarget An array of bytes to match + /// \param offset An offset to perform the match at + /// \return True if this MappedName begins with the target bytes bool startsWith(const char* searchTarget, int offset = 0) const { if (!searchTarget) { @@ -676,6 +869,13 @@ public: QByteArray::fromRawData(searchTarget, static_cast(qstrlen(searchTarget))), offset); } + /// Returns true if this MappedName starts with the search target. If there is a postfix, only + /// the postfix is considered. If not, then only the data is considered. A search string that + /// overlaps the two will not be found. + /// + /// \param searchTarget A string to match + /// \param offset An offset to perform the match at + /// \return True if this MappedName begins with the target bytes bool startsWith(const std::string& searchTarget, int offset = 0) const { return startsWith( @@ -683,6 +883,7 @@ public: offset); } + /// Get a hash for this MappedName std::size_t hash() const { return qHash(data, qHash(postfix));