execute method

  1. @override
Future<ToolResult> execute(
  1. Map<String, dynamic> input
)
override

Execute the tool with the given input.

Implementation

@override
Future<ToolResult> execute(Map<String, dynamic> input) async {
  final filePath = input['file_path'] as String?;
  final content = input['content'] as String?;

  // Validate required parameters
  if (filePath == null || filePath.isEmpty) {
    return ToolResult.error('Missing required parameter: file_path');
  }
  if (!p.isAbsolute(filePath)) {
    return ToolResult.error(
      'file_path must be an absolute path, got: $filePath',
    );
  }
  if (content == null) {
    return ToolResult.error('Missing required parameter: content');
  }

  // Check content size
  if (content.length > maxContentSize) {
    return ToolResult.error(
      'Content too large: ${_formatFileSize(content.length)} '
      '(max ${_formatFileSize(maxContentSize)})',
    );
  }

  // Check protected paths
  final protectedCheck = _checkProtectedPath(filePath);
  if (protectedCheck != null) {
    return ToolResult.error(protectedCheck);
  }

  // Resolve symlinks for the target path
  final resolvedPath = await _resolveSymlink(filePath);

  final file = File(resolvedPath);
  final isNewFile = !await file.exists();
  String? backupPath;

  try {
    // Create parent directories if needed
    final parent = file.parent;
    if (!await parent.exists()) {
      await parent.create(recursive: true);
    }

    // Check parent directory write permission
    if (!await _isWritable(parent.path)) {
      return ToolResult.error(
        'Permission denied: Cannot write to directory '
        '${parent.path}',
      );
    }

    // Backup existing file before overwriting
    if (!isNewFile) {
      backupPath = await _createBackup(file);
    }

    // Atomic write: write to temp file, then rename
    final tempFile = File('$resolvedPath.tmp.${_timestamp()}');
    try {
      await tempFile.writeAsString(content, encoding: utf8, flush: true);

      // Post-write verification: read back and compare
      final verifyContent = await tempFile.readAsString(encoding: utf8);
      if (verifyContent != content) {
        await tempFile.delete();
        return ToolResult.error(
          'Post-write verification failed: content mismatch. '
          'The write was aborted.',
        );
      }

      // Rename temp file to target (atomic on same filesystem)
      await tempFile.rename(resolvedPath);
    } catch (e) {
      // Clean up temp file on failure
      if (await tempFile.exists()) {
        await tempFile.delete();
      }
      rethrow;
    }

    // Get final file stats
    final stat = await file.stat();
    final bytesWritten = stat.size;

    final output = FileWriteOutput(
      success: true,
      message: isNewFile
          ? 'New file created: $filePath'
          : 'File overwritten: $filePath',
      bytesWritten: bytesWritten,
      created: isNewFile,
      backupPath: backupPath,
    );

    final resultBuf = StringBuffer();
    resultBuf.writeln(output.message);
    resultBuf.writeln('Size: ${_formatFileSize(bytesWritten)}');
    if (!isNewFile && backupPath != null) {
      resultBuf.writeln('Backup: $backupPath');
    }

    return ToolResult.success(
      resultBuf.toString(),
      metadata: output.toMetadata(),
    );
  } catch (e) {
    // Attempt to restore from backup on failure
    if (backupPath != null && await File(backupPath).exists()) {
      try {
        await File(backupPath).copy(resolvedPath);
      } catch (_) {
        // Restoration failed too
      }
    }
    return ToolResult.error('Error writing file: $e');
  }
}