* @brief Reads a byte slice of a file under the read roots, paged via offset/limit. */
| 403 | * @brief Reads a byte slice of a file under the read roots, paged via offset/limit. |
| 404 | */ |
| 405 | QJsonObject AI::FileSandbox::read(const QJsonObject& args) const |
| 406 | { |
| 407 | const auto resolved = resolveRead(args.value(QStringLiteral("path")).toString()); |
| 408 | if (!resolved.ok) |
| 409 | return failure(resolved.error, resolved.hint); |
| 410 | |
| 411 | QFileInfo info(resolved.path); |
| 412 | if (!info.isFile()) |
| 413 | return failure(QStringLiteral("not_a_file"), |
| 414 | QStringLiteral("Use fs.list for directories; fs.read expects a file.")); |
| 415 | |
| 416 | QFile file(resolved.path); |
| 417 | if (!file.open(QIODevice::ReadOnly)) |
| 418 | return failure(QStringLiteral("open_failed"), file.errorString()); |
| 419 | |
| 420 | if (info.isSymLink() || !openedPathWithinRoots(file.fileName(), readRoots())) |
| 421 | return failure(QStringLiteral("outside_sandbox"), |
| 422 | QStringLiteral("Refusing to read a symlinked or sandbox-escaping path.")); |
| 423 | |
| 424 | const qint64 total = file.size(); |
| 425 | const qint64 offset = qBound<qint64>(0, args.value(QStringLiteral("offset")).toInteger(0), total); |
| 426 | const qint64 want = args.value(QStringLiteral("limit")).toInt(kMaxReadSlice); |
| 427 | const qint64 limit = qBound<qint64>(1, want, kMaxReadSlice); |
| 428 | |
| 429 | if (!file.seek(offset)) |
| 430 | return failure(QStringLiteral("seek_failed"), file.errorString()); |
| 431 | |
| 432 | const QByteArray sniff = file.peek(qMin<qint64>(limit, kBinarySniffBytes)); |
| 433 | if (looksBinary(sniff)) |
| 434 | return failure(QStringLiteral("binary_file"), |
| 435 | QStringLiteral("File appears binary. fs.read returns text only; request a " |
| 436 | "different file or a known text range.")); |
| 437 | |
| 438 | const QByteArray chunk = file.read(limit); |
| 439 | const qint64 next = offset + chunk.size(); |
| 440 | |
| 441 | QJsonObject out; |
| 442 | out[QStringLiteral("ok")] = true; |
| 443 | out[QStringLiteral("path")] = displayPath(resolved.path, workspaceRoot()); |
| 444 | out[QStringLiteral("content")] = QString::fromUtf8(chunk); |
| 445 | out[QStringLiteral("offset")] = static_cast<double>(offset); |
| 446 | out[QStringLiteral("bytesReturned")] = static_cast<double>(chunk.size()); |
| 447 | out[QStringLiteral("totalBytes")] = static_cast<double>(total); |
| 448 | out[QStringLiteral("eof")] = (next >= total); |
| 449 | if (next < total) |
| 450 | out[QStringLiteral("nextOffset")] = static_cast<double>(next); |
| 451 | |
| 452 | return out; |
| 453 | } |
| 454 | |
| 455 | //-------------------------------------------------------------------------------------------------- |
| 456 | // search |
no test coverage detected