综合平台编程器项目的远程存储
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

235 líneas
7.0 KiB

  1. /**
  2. * @file runtime_project_bundle.cpp
  3. * @brief 实现用户运行程序工程数据封装服务
  4. * @version 1.0.0
  5. * @author suyu
  6. * @date 2026-08-26
  7. */
  8. #include "runtime_project_bundle.h"
  9. #include <QCryptographicHash>
  10. #include <QFile>
  11. #include <QFileInfo>
  12. #include <QObject>
  13. #include <limits>
  14. namespace {
  15. constexpr int kMagicSize = 16;
  16. constexpr int kFooterSize = kMagicSize + 4 + 8 + 32;
  17. constexpr quint32 kFormatVersion = 1;
  18. constexpr qint64 kCopyBufferSize = 64 * 1024;
  19. const QByteArray kMagic = QByteArrayLiteral("QTPROX_RUNTIME_B");
  20. static_assert(sizeof("QTPROX_RUNTIME_B") - 1 == kMagicSize,
  21. "runtime bundle magic must have a fixed length");
  22. void appendUint32LittleEndian(QByteArray *bytes, quint32 value)
  23. {
  24. for (int shift = 0; shift < 32; shift += 8)
  25. {
  26. bytes->append(static_cast<char>((value >> shift) & 0xff));
  27. }
  28. }
  29. void appendUint64LittleEndian(QByteArray *bytes, quint64 value)
  30. {
  31. for (int shift = 0; shift < 64; shift += 8)
  32. {
  33. bytes->append(static_cast<char>((value >> shift) & 0xff));
  34. }
  35. }
  36. quint32 readUint32LittleEndian(const char *bytes)
  37. {
  38. quint32 value = 0;
  39. for (int shift = 0; shift < 32; shift += 8)
  40. {
  41. value |= static_cast<quint32>(
  42. static_cast<unsigned char>(bytes[shift / 8]))
  43. << shift;
  44. }
  45. return value;
  46. }
  47. quint64 readUint64LittleEndian(const char *bytes)
  48. {
  49. quint64 value = 0;
  50. for (int shift = 0; shift < 64; shift += 8)
  51. {
  52. value |= static_cast<quint64>(
  53. static_cast<unsigned char>(bytes[shift / 8]))
  54. << shift;
  55. }
  56. return value;
  57. }
  58. QByteArray makeFooter(quint64 project_size, const QByteArray &hash)
  59. {
  60. QByteArray footer;
  61. footer.reserve(kFooterSize);
  62. footer.append(kMagic);
  63. appendUint32LittleEndian(&footer, kFormatVersion);
  64. appendUint64LittleEndian(&footer, project_size);
  65. footer.append(hash);
  66. return footer;
  67. }
  68. RuntimeProjectBundleLoadResult invalidBundle(const QString &message)
  69. {
  70. return {RuntimeProjectBundleStatus::Invalid, {}, message};
  71. }
  72. } // namespace
  73. RuntimeProjectBundleWriteResult RuntimeProjectBundleService::write(
  74. const QString &template_executable,
  75. const QString &project_file,
  76. const QString &destination_executable)
  77. {
  78. if (!QFileInfo::exists(template_executable))
  79. {
  80. return {false,
  81. QObject::tr("未找到用户运行程序模板:%1")
  82. .arg(template_executable)};
  83. }
  84. if (!QFileInfo::exists(project_file))
  85. {
  86. return {false,
  87. QObject::tr("未找到待封装的工程文件:%1").arg(project_file)};
  88. }
  89. if (QFileInfo::exists(destination_executable))
  90. {
  91. return {false,
  92. QObject::tr("导出 exe 已经存在:%1")
  93. .arg(destination_executable)};
  94. }
  95. const QString temporary_executable = destination_executable
  96. + QStringLiteral(".runtime-bundle-tmp");
  97. QFile::remove(temporary_executable);
  98. if (!QFile::copy(template_executable, temporary_executable))
  99. {
  100. return {false,
  101. QObject::tr("复制用户运行程序模板失败:%1")
  102. .arg(template_executable)};
  103. }
  104. QFile output(temporary_executable);
  105. QFile project(project_file);
  106. if (!output.open(QIODevice::ReadWrite | QIODevice::Append)
  107. || !project.open(QIODevice::ReadOnly))
  108. {
  109. output.close();
  110. project.close();
  111. QFile::remove(temporary_executable);
  112. return {false, QObject::tr("无法打开用户运行程序封装文件")};
  113. }
  114. QCryptographicHash hash(QCryptographicHash::Sha256);
  115. quint64 project_size = 0;
  116. bool succeeded = true;
  117. while (!project.atEnd())
  118. {
  119. const QByteArray chunk = project.read(kCopyBufferSize);
  120. if (chunk.isEmpty() && project.error() != QFileDevice::NoError)
  121. {
  122. succeeded = false;
  123. break;
  124. }
  125. if (chunk.isEmpty())
  126. {
  127. break;
  128. }
  129. if (output.write(chunk) != chunk.size())
  130. {
  131. succeeded = false;
  132. break;
  133. }
  134. hash.addData(chunk);
  135. project_size += static_cast<quint64>(chunk.size());
  136. }
  137. if (succeeded)
  138. {
  139. const QByteArray footer = makeFooter(project_size, hash.result());
  140. succeeded = output.write(footer) == footer.size();
  141. }
  142. output.flush();
  143. output.close();
  144. project.close();
  145. if (!succeeded || !QFile::rename(temporary_executable, destination_executable))
  146. {
  147. QFile::remove(temporary_executable);
  148. return {false, QObject::tr("写入用户运行程序工程数据失败")};
  149. }
  150. return {true, {}};
  151. }
  152. RuntimeProjectBundleLoadResult RuntimeProjectBundleService::load(
  153. const QString &executable_path)
  154. {
  155. QFile executable(executable_path);
  156. if (!executable.open(QIODevice::ReadOnly))
  157. {
  158. return invalidBundle(
  159. QObject::tr("无法读取用户运行程序:%1").arg(executable_path));
  160. }
  161. const qint64 executable_size = executable.size();
  162. if (executable_size < kFooterSize)
  163. {
  164. return {RuntimeProjectBundleStatus::NotFound, {}, {}};
  165. }
  166. if (!executable.seek(executable_size - kFooterSize))
  167. {
  168. return invalidBundle(QObject::tr("无法定位用户运行程序工程数据"));
  169. }
  170. const QByteArray footer = executable.read(kFooterSize);
  171. if (footer.size() != kFooterSize)
  172. {
  173. return invalidBundle(QObject::tr("用户运行程序工程数据不完整"));
  174. }
  175. if (footer.left(kMagicSize) != kMagic)
  176. {
  177. return {RuntimeProjectBundleStatus::NotFound, {}, {}};
  178. }
  179. const quint32 version = readUint32LittleEndian(footer.constData() + kMagicSize);
  180. const quint64 project_size = readUint64LittleEndian(
  181. footer.constData() + kMagicSize + 4);
  182. if (version != kFormatVersion)
  183. {
  184. return invalidBundle(QObject::tr("不支持的用户运行程序工程数据版本"));
  185. }
  186. if (project_size > static_cast<quint64>(
  187. executable_size - static_cast<qint64>(kFooterSize))
  188. || project_size > static_cast<quint64>(std::numeric_limits<qint64>::max()))
  189. {
  190. return invalidBundle(QObject::tr("用户运行程序工程数据长度无效"));
  191. }
  192. const qint64 project_offset = executable_size - kFooterSize
  193. - static_cast<qint64>(project_size);
  194. if (!executable.seek(project_offset))
  195. {
  196. return invalidBundle(QObject::tr("无法读取用户运行程序工程数据"));
  197. }
  198. const QByteArray project_data = executable.read(
  199. static_cast<qint64>(project_size));
  200. if (project_data.size() != static_cast<qint64>(project_size))
  201. {
  202. return invalidBundle(QObject::tr("用户运行程序工程数据不完整"));
  203. }
  204. const QByteArray expected_hash = footer.right(32);
  205. if (QCryptographicHash::hash(project_data, QCryptographicHash::Sha256)
  206. != expected_hash)
  207. {
  208. return invalidBundle(QObject::tr("用户运行程序工程数据校验失败"));
  209. }
  210. return {RuntimeProjectBundleStatus::Loaded, project_data, {}};
  211. }