Skip to content

Commit fa0511a

Browse files
committed
fix: Refactor file saving with TextFileSaver and add safety checks
- Added TextFileSaver class for atomic file saving with encoding conversion - Refactored all file saving operations in EditWrapper to use TextFileSaver - Added null checks in Window class for safer wrapper access - Improved error handling for long filenames and permission issues - Removed duplicate encoding conversion code paths Log: Improved file saving reliability and added safety checks throughout the editor
1 parent 4a4ad41 commit fa0511a

7 files changed

Lines changed: 448 additions & 80 deletions

File tree

src/common/text_file_saver.cpp

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
/**
6+
* @file file_saver.cpp
7+
* @brief Implementation of FileSaver class for safe file saving with encoding conversion
8+
*/
9+
10+
#include "text_file_saver.h"
11+
#include "../encodes/detectcode.h"
12+
13+
#include <QSaveFile>
14+
#include <QFileInfo>
15+
#include <QTextCodec>
16+
#include <QDebug>
17+
#include <QIODevice>
18+
#include <QTextDocument>
19+
#include <QObject>
20+
21+
/**
22+
* @brief Constructs a TextFileSaver with the given text document
23+
* @param document The QTextDocument to be saved
24+
*/
25+
TextFileSaver::TextFileSaver(QTextDocument *document)
26+
: m_document(document)
27+
, m_fromEncode("UTF-16")
28+
, m_toEncode("UTF-8")
29+
{
30+
}
31+
32+
TextFileSaver::~TextFileSaver() {}
33+
34+
/**
35+
* @brief Sets the target file path for saving
36+
* @param filePath The full path of the file to save to
37+
*/
38+
void TextFileSaver::setFilePath(const QString &filePath)
39+
{
40+
m_filePath = filePath;
41+
}
42+
43+
/**
44+
* @brief Sets the target encoding for the saved file
45+
* @param toEncode The target encoding for the saved file
46+
*/
47+
void TextFileSaver::setEncoding(const QByteArray &toEncode)
48+
{
49+
m_toEncode = toEncode;
50+
}
51+
52+
/**
53+
* @brief Saves the document to the preset file path
54+
* @return true if the file was saved successfully, false otherwise
55+
* @note Uses QSaveFile for atomic writes when filename is not too long
56+
*/
57+
bool TextFileSaver::save()
58+
{
59+
if (m_filePath.isEmpty()) {
60+
m_errorString = QObject::tr("File path is empty");
61+
return false;
62+
}
63+
64+
// WARNING: For long filenames (>245 chars), QSaveFile may create temporary files
65+
// with names that exceed system limits. TextFileSaver handles this internally.
66+
QFileInfo fileInfo(m_filePath);
67+
bool disableSaveProtect = fileInfo.fileName().length() > MAX_FILENAME_LENGTH;
68+
69+
if (!disableSaveProtect) {
70+
QSaveFile saveFile(m_filePath);
71+
saveFile.setDirectWriteFallback(true);
72+
if (!saveToFile(saveFile)) {
73+
return false;
74+
}
75+
return saveFile.commit();
76+
} else {
77+
qWarning() << "File name too long, disable QSaveFile. path:" << m_filePath;
78+
QFile file(m_filePath);
79+
if (!saveToFile(file)) {
80+
return false;
81+
}
82+
return true;
83+
}
84+
}
85+
86+
/**
87+
* @brief Saves the document to a new file path
88+
* @param newFilePath The target path to save the file to
89+
* @return true if the file was saved successfully, false otherwise
90+
* @note Restores original file path if save fails
91+
*/
92+
bool TextFileSaver::saveAs(const QString &newFilePath)
93+
{
94+
QString oldPath = m_filePath;
95+
m_filePath = newFilePath;
96+
bool result = save();
97+
if (!result) {
98+
m_filePath = oldPath;
99+
}
100+
return result;
101+
}
102+
103+
/**
104+
* @brief Gets the last error message
105+
* @return The description of the last error that occurred
106+
*/
107+
QString TextFileSaver::errorString() const
108+
{
109+
return m_errorString;
110+
}
111+
112+
/**
113+
* @brief Internal implementation of file writing
114+
* @param file The QFileDevice to write to
115+
* @return true if the write was successful, false otherwise
116+
*/
117+
bool TextFileSaver::saveToFile(QFileDevice &file)
118+
{
119+
try {
120+
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
121+
m_errorString = file.errorString();
122+
return false;
123+
}
124+
125+
const QString content = m_document->toPlainText();
126+
const ushort *data = content.utf16();
127+
const int length = content.length();
128+
129+
// Dynamically calculate chunk size (10MB or 1/8 of total length, whichever is larger)
130+
const int chunkSize = qMax(10 * 1024 * 1024, length / 8);
131+
132+
for (int i = 0; i < length; i += chunkSize) {
133+
int currentChunkSize = qMin(chunkSize, length - i);
134+
QByteArray input(reinterpret_cast<const char *>(data + i), currentChunkSize * sizeof(ushort));
135+
QByteArray outData;
136+
137+
if (!convertEncoding(input, outData)) {
138+
m_errorString = QObject::tr("Encoding conversion failed");
139+
return false;
140+
}
141+
142+
if (outData.isEmpty()) {
143+
m_errorString = QObject::tr("Converted content is empty");
144+
return false;
145+
}
146+
147+
qint64 written = file.write(outData);
148+
if (written != outData.size()) {
149+
m_errorString = file.errorString();
150+
return false;
151+
}
152+
}
153+
154+
return true;
155+
} catch (const std::bad_alloc &) {
156+
m_errorString = QObject::tr("Memory allocation failed");
157+
return false;
158+
} catch (const std::exception &e) {
159+
m_errorString = QObject::tr("Error occurred: %1").arg(e.what());
160+
return false;
161+
} catch (...) {
162+
m_errorString = QObject::tr("Unknown error occurred");
163+
return false;
164+
}
165+
}
166+
167+
/**
168+
* @brief Gets the document content as plain text
169+
* @return The document content as QString
170+
*/
171+
QString TextFileSaver::getDocumentContent() const
172+
{
173+
return m_document->toPlainText();
174+
}
175+
176+
/**
177+
* @brief Converts between character encodings
178+
* @param input The input byte array to convert
179+
* @param output The converted output byte array
180+
* @return true if conversion was successful, false otherwise
181+
* @note Uses DetectCode first, falls back to QTextCodec if needed
182+
*/
183+
bool TextFileSaver::convertEncoding(QByteArray &input, QByteArray &output) const
184+
{
185+
if (m_fromEncode == m_toEncode) {
186+
output = input;
187+
return true;
188+
}
189+
190+
if (!DetectCode::ChangeFileEncodingFormat(input, output, m_fromEncode, m_toEncode)) {
191+
return false;
192+
}
193+
return true;
194+
}

src/common/text_file_saver.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: GPL-3.0-or-later
4+
5+
#ifndef TEXT_FILE_SAVER_H
6+
#define TEXT_FILE_SAVER_H
7+
8+
#include <QString>
9+
#include <QByteArray>
10+
#include <QFile>
11+
12+
class QTextDocument;
13+
14+
class TextFileSaver
15+
{
16+
public:
17+
explicit TextFileSaver(QTextDocument *document);
18+
~TextFileSaver();
19+
20+
void setFilePath(const QString &filePath);
21+
void setEncoding(const QByteArray &toEncode);
22+
bool save();
23+
bool saveAs(const QString &newFilePath);
24+
QString errorString() const;
25+
26+
private:
27+
bool saveToFile(QFileDevice &file);
28+
QString getDocumentContent() const;
29+
bool convertEncoding(QByteArray &input, QByteArray &output) const;
30+
31+
private:
32+
QTextDocument *m_document;
33+
QString m_filePath;
34+
QByteArray m_fromEncode; // default is UTF-16 (QString)
35+
QByteArray m_toEncode; // default is UTF-8
36+
QString m_errorString;
37+
static const int MAX_FILENAME_LENGTH = 245;
38+
};
39+
40+
#endif // TEXT_FILE_SAVER_H

src/common/utils.cpp

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,3 +1122,108 @@ void Utils::sendFloatMessageFixedFont(QWidget *par, const QIcon &icon, const QSt
11221122

11231123
DMessageManager::instance()->sendMessage(par, floMsg);
11241124
}
1125+
1126+
/**
1127+
* @brief Gets system memory information from /proc/meminfo
1128+
* @param[out] totalMemory Total system memory in KB
1129+
* @param[out] freeMemory Available memory in KB (MemAvailable if exists, otherwise MemFree+Buffers+Cached)
1130+
* @return True if memory info was successfully read and parsed, false otherwise
1131+
*/
1132+
bool Utils::getSystemMemoryInfo(qlonglong &totalMemory, qlonglong &freeMemory)
1133+
{
1134+
qlonglong memFree = 0;
1135+
qlonglong buffers = 0;
1136+
qlonglong cached = 0;
1137+
bool valueOk = false;
1138+
1139+
QFile file(PROC_MEMINFO_PATH);
1140+
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1141+
qWarning() << "Utils: Failed to open" << PROC_MEMINFO_PATH << "for memory check.";
1142+
return false;
1143+
}
1144+
1145+
QByteArray allData = file.readAll();
1146+
file.close();
1147+
1148+
QTextStream stream(allData);
1149+
QString line;
1150+
while (!stream.atEnd()) {
1151+
line = stream.readLine();
1152+
if (line.startsWith("MemTotal:")) {
1153+
totalMemory = line.section(':', 1, 1).trimmed().section(' ', 0, 0).toLongLong(&valueOk);
1154+
} else if (line.startsWith("MemFree:")) {
1155+
memFree = line.section(':', 1, 1).trimmed().section(' ', 0, 0).toLongLong(&valueOk);
1156+
} else if (line.startsWith("Buffers:")) {
1157+
buffers = line.section(':', 1, 1).trimmed().section(' ', 0, 0).toLongLong(&valueOk);
1158+
} else if (line.startsWith("Cached:")) {
1159+
cached = line.section(':', 1, 1).trimmed().section(' ', 0, 0).toLongLong(&valueOk);
1160+
} else if (line.startsWith("MemAvailable:")) {
1161+
freeMemory = line.section(':', 1, 1).trimmed().section(' ', 0, 0).toLongLong(&valueOk);
1162+
break;
1163+
}
1164+
if (stream.atEnd() && freeMemory == 0) {
1165+
freeMemory = memFree + buffers + cached;
1166+
}
1167+
}
1168+
1169+
return (totalMemory > 0 && freeMemory > 0);
1170+
}
1171+
1172+
/**
1173+
* @brief Checks if the system has sufficient memory to perform the specified operation.
1174+
* @param operationType The type of operation (Copy/Paste).
1175+
* @param operationDataSize The size of the data involved in the operation (bytes).
1176+
* @param currentDocumentSize The current size of the document (characters/bytes).
1177+
* @return True if memory is sufficient, false otherwise.
1178+
*/
1179+
bool Utils::isMemorySufficientForOperation(OperationType operationType, qlonglong operationDataSize, qlonglong currentDocumentSize)
1180+
{
1181+
qlonglong memoryFree = 0;
1182+
qlonglong memoryTotal = 0;
1183+
1184+
if (!getSystemMemoryInfo(memoryTotal, memoryFree)) {
1185+
// Conservatively allow the operation if memory info cannot be read
1186+
return true;
1187+
}
1188+
1189+
// Convert KB to Bytes for comparison
1190+
qlonglong availableMemoryBytes = memoryFree * DATA_SIZE_1024;
1191+
qlonglong totalMemoryBytes = memoryTotal * DATA_SIZE_1024;
1192+
1193+
// Judge based on operation type
1194+
if (operationType == OperationType::CopyOperation) {
1195+
// Copy operation: Estimated memory consumption = data size * factor
1196+
qlonglong estimatedMemoryNeeded = operationDataSize * COPY_CONSUME_MEMORY_MULTIPLE;
1197+
if (estimatedMemoryNeeded > availableMemoryBytes) {
1198+
qWarning() << "Utils: Insufficient memory for copy operation. Needed(est):" << estimatedMemoryNeeded << "Available:" << availableMemoryBytes;
1199+
return false;
1200+
}
1201+
} else if (operationType == OperationType::PasteOperation) {
1202+
// Paste operation: Estimated memory consumption = paste data size * factor
1203+
qlonglong estimatedMemoryNeededForPaste = operationDataSize * PASTE_CONSUME_MEMORY_MULTIPLE;
1204+
// Estimated total document memory after paste
1205+
qlonglong estimatedTotalDocMemory = (currentDocumentSize + operationDataSize) * PASTE_CONSUME_MEMORY_MULTIPLE; // Estimate using paste factor
1206+
1207+
// Check if pasting the data itself would cause insufficient memory
1208+
if (estimatedMemoryNeededForPaste > availableMemoryBytes) {
1209+
qWarning() << "Utils: Insufficient memory for paste operation (paste data). Needed(est):" << estimatedMemoryNeededForPaste << "Available:" << availableMemoryBytes;
1210+
return false;
1211+
}
1212+
1213+
// Check if the estimated total document size after paste exceeds total system memory (very rough check)
1214+
if (estimatedTotalDocMemory > totalMemoryBytes) {
1215+
qWarning() << "Utils: Paste operation might exceed total system memory. Estimated total doc memory:" << estimatedTotalDocMemory << "Total system memory:" << totalMemoryBytes;
1216+
return false;
1217+
}
1218+
1219+
// Check specific threshold: Restrict paste size if document reaches 800MB
1220+
const qlonglong DOC_SIZE_LIMIT_800MB = 800LL * DATA_SIZE_1024 * DATA_SIZE_1024;
1221+
const qlonglong PASTE_SIZE_LIMIT_500KB = 500LL * DATA_SIZE_1024;
1222+
if (currentDocumentSize > DOC_SIZE_LIMIT_800MB && operationDataSize > PASTE_SIZE_LIMIT_500KB) {
1223+
qWarning() << "Utils: Paste operation restricted. Document size exceeds 800MB and paste data exceeds 500KB.";
1224+
return false;
1225+
}
1226+
}
1227+
1228+
return true; // Memory is sufficient
1229+
}

src/common/utils.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ class Utils
4747
EIntersectInner, ///< 活动区间处于固定区间内部 例如 [0, 9] 和 [5, 6]
4848
};
4949

50+
/**
51+
* @brief A text operation type for memory checking
52+
*/
53+
enum OperationType {
54+
CopyOperation,
55+
PasteOperation
56+
};
57+
5058
/**
5159
* @brief 当前运行时系统环境
5260
*/
@@ -157,6 +165,11 @@ class Utils
157165
// 发送浮动提示信息,并且字体大小跟随 qApp 应用默认字体而不是父窗口字体
158166
static void sendFloatMessageFixedFont(QWidget *par, const QIcon &icon, const QString &message);
159167

168+
// Gets system memory information from /proc/meminfo
169+
static bool getSystemMemoryInfo(qlonglong &totalMemory, qlonglong &freeMemory);
170+
// Checks if the system has sufficient memory to perform the specified operation.
171+
static bool isMemorySufficientForOperation(OperationType operationType, qlonglong operationDataSize, qlonglong currentDocumentSize);
172+
160173
private:
161174
static QString m_systemLanguage;
162175
};

0 commit comments

Comments
 (0)