class RubySMB::SMB1::File

Represents a file on the Remote server that we can perform various I/O operations on.

Attributes

attributes[RW]

The {SmbExtFileAttributes} for the file @!attribute [rw] attributes

@return [RubySMB::SMB1::BitField::SmbExtFileAttributes]
fid[RW]

The file ID @!attribute [rw] fid

@return [Integer]
last_access[RW]

The last access date/time for the file @!attribute [rw] last_access

@return [DateTime]
last_change[RW]

The last change date/time for the file @!attribute [rw] last_change

@return [DateTime]
last_write[RW]

The last write date/time for the file @!attribute [rw] last_write

@return [DateTime]
name[RW]

The name of the file @!attribute [rw] name

@return [String]
size[RW]

The actual size, in bytes, of the file @!attribute [rw] size

@return [Integer]
size_on_disk[RW]

The size in bytes that the file occupies on disk @!attribute [rw] size_on_disk

@return [Integer]
tree[RW]

The {RubySMB::SMB1::Tree} that this file belong to @!attribute [rw] tree

@return [RubySMB::SMB1::Tree]

Public Class Methods

new(tree:, response:, name:) click to toggle source
# File lib/ruby_smb/smb1/file.rb, line 51
def initialize(tree:, response:, name:)
  raise ArgumentError, 'No tree provided' if tree.nil?
  raise ArgumentError, 'No response provided' if response.nil?
  raise ArgumentError, 'No file name provided' if name.nil?

  @tree = tree
  @name = name

  @attributes   = response.parameter_block.ext_file_attributes
  @fid          = response.parameter_block.fid
  @last_access  = response.parameter_block.last_access_time.to_datetime
  @last_change  = response.parameter_block.last_change_time.to_datetime
  @last_write   = response.parameter_block.last_write_time.to_datetime
  @size         = response.parameter_block.end_of_file
  @size_on_disk = response.parameter_block.allocation_size
end

Public Instance Methods

append(data:) click to toggle source

Appends the supplied data to the end of the file.

@param data [String] the data to write to the file @return [WindowsError::ErrorCode] the NTStatus code returned from the operation

# File lib/ruby_smb/smb1/file.rb, line 72
def append(data:)
  write(data: data, offset: @size)
end
close() click to toggle source

Closes the handle to the remote file.

@return [WindowsError::ErrorCode] the NTStatus code returned by the operation @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb1/file.rb, line 81
def close
  close_request = set_header_fields(RubySMB::SMB1::Packet::CloseRequest.new)
  raw_response  = @tree.client.send_recv(close_request)
  response = RubySMB::SMB1::Packet::CloseResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::CloseResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end
  response.status_code
end
delete() click to toggle source

Delete a file on close

@return [WindowsError::ErrorCode] the NTStatus Response code @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid

# File lib/ruby_smb/smb1/file.rb, line 182
def delete
  raw_response = @tree.client.send_recv(delete_packet)
  response = RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse::COMMAND,
      packet:         response
    )
  end
  response.status_code
end
delete_packet() click to toggle source

Crafts the SetFileInformationRequest packet to be sent for delete operations.

@return [RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest] the set info packet

# File lib/ruby_smb/smb1/file.rb, line 198
def delete_packet
  delete_request = RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest.new
  delete_request = @tree.set_header_fields(delete_request)
  delete_request.data_block.trans2_parameters.fid = @fid
  passthrough_info_level = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION +
    RubySMB::Fscc::FileInformation::SMB_INFO_PASSTHROUGH
  delete_request.data_block.trans2_parameters.information_level = passthrough_info_level
  delete_request.data_block.trans2_data.info_level_struct.delete_pending = 1
  set_trans2_params(delete_request)
end
read(bytes: @size, offset: 0) click to toggle source

Read from the file, a specific number of bytes from a specific offset. If no parameters are given it will read the entire file.

@param bytes [Integer] the number of bytes to read @param offset [Integer] the byte offset in the file to start reading from @return [String] the data read from the file @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb1/file.rb, line 107
def read(bytes: @size, offset: 0)
  atomic_read_size = [bytes, @tree.client.max_buffer_size].min
  remaining_bytes = bytes
  data = ''

  loop do
    read_request = read_packet(read_length: atomic_read_size, offset: offset)
    raw_response = @tree.client.send_recv(read_request)
    response = RubySMB::SMB1::Packet::ReadAndxResponse.read(raw_response)
    unless response.valid?
      raise RubySMB::Error::InvalidPacket.new(
        expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
        expected_cmd:   RubySMB::SMB1::Packet::ReadAndxResponse::COMMAND,
        packet:         response
      )
    end
    unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
      raise RubySMB::Error::UnexpectedStatusCode, response.status_code
    end

    if response.is_a?(RubySMB::SMB1::Packet::ReadAndxResponse)
      data << response.data_block.data.to_binary_s
    else
      # Returns the current data immediately if we got an empty packet with an
      # SMB_COM_READ_ANDX command and a STATUS_SUCCESS (just in case)
      return data
    end

    remaining_bytes -= atomic_read_size
    break unless remaining_bytes > 0

    offset += atomic_read_size
    atomic_read_size = remaining_bytes if remaining_bytes < @tree.client.max_buffer_size
  end

  data
end
read_packet(read_length: 0, offset: 0) click to toggle source

Crafts the ReadRequest packet to be sent for read operations.

@param bytes [Integer] the number of bytes to read @param offset [Integer] the byte offset in the file to start reading from @return [RubySMB::SMB1::Packet::ReadAndxRequest] the crafted ReadRequest packet

# File lib/ruby_smb/smb1/file.rb, line 150
def read_packet(read_length: 0, offset: 0)
  read_request = set_header_fields(RubySMB::SMB1::Packet::ReadAndxRequest.new)
  read_request.parameter_block.max_count_of_bytes_to_return = read_length
  read_request.parameter_block.min_count_of_bytes_to_return = read_length
  read_request.parameter_block.remaining = read_length
  read_request.parameter_block.offset = offset
  read_request
end
rename(new_file_name) click to toggle source

Rename a file

@param new_file_name [String] the new name @return [WindowsError::ErrorCode] the NTStatus Response code @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid

# File lib/ruby_smb/smb1/file.rb, line 280
def rename(new_file_name)
  raw_response = tree.client.send_recv(rename_packet(new_file_name))
  response = RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::Trans2::SetFileInformationResponse::COMMAND,
      packet:         response
    )
  end
  response.status_code
end
rename_packet(new_file_name) click to toggle source

Crafts the SetFileInformationRequest packet to be sent for rename operations.

@param new_file_name [String] the new name @return [RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest] the set info packet

# File lib/ruby_smb/smb1/file.rb, line 297
def rename_packet(new_file_name)
  rename_request = RubySMB::SMB1::Packet::Trans2::SetFileInformationRequest.new
  rename_request = @tree.set_header_fields(rename_request)
  rename_request.data_block.trans2_parameters.fid = @fid
  passthrough_info_level = RubySMB::Fscc::FileInformation::FILE_RENAME_INFORMATION +
    RubySMB::Fscc::FileInformation::SMB_INFO_PASSTHROUGH
  rename_request.data_block.trans2_parameters.information_level = passthrough_info_level
  rename_request.data_block.trans2_data.info_level_struct.file_name = new_file_name
  set_trans2_params(rename_request)
end
send_recv_read(read_length: 0, offset: 0) click to toggle source
# File lib/ruby_smb/smb1/file.rb, line 159
def send_recv_read(read_length: 0, offset: 0)
  read_request = read_packet(read_length: read_length, offset: offset)
  raw_response = tree.client.send_recv(read_request)

  response = RubySMB::SMB1::Packet::ReadAndxResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::ReadAndxResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end

  response.data_block.data.to_binary_s
end
send_recv_write(data:'', offset: 0) click to toggle source
# File lib/ruby_smb/smb1/file.rb, line 260
def send_recv_write(data:'', offset: 0)
  pkt = write_packet(data: data, offset: offset)
  pkt.set_64_bit_offset(true)
  raw_response = @tree.client.send_recv(pkt)
  response = RubySMB::SMB1::Packet::WriteAndxResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB1::Packet::WriteAndxResponse::COMMAND,
      packet:         response
    )
  end
  response.parameter_block.count_low
end
set_header_fields(request) click to toggle source

Sets the header fields that we have to set on every packet we send for File operations.

@param request [RubySMB::GenericPacket] the request packet to set fields on @return [RubySMB::GenericPacket] the modified request packet

# File lib/ruby_smb/smb1/file.rb, line 313
def set_header_fields(request)
  request = @tree.set_header_fields(request)
  request.parameter_block.fid = @fid
  request
end
write(data:, offset: 0) click to toggle source

Write the supplied data to the file at the given offset.

@param data [String] the data to write to the file @param offset [Integer] the offset in the file to start writing from @return [Integer] the count of bytes written @raise [RubySMB::Error::InvalidPacket] if the response packet is not valid @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb1/file.rb, line 216
def write(data:, offset: 0)
  buffer = data.dup
  bytes  = data.length
  total_bytes_written = 0

  loop do
    atomic_write_size = [bytes, @tree.client.max_buffer_size].min
    write_request = write_packet(data: buffer.slice!(0, atomic_write_size), offset: offset)
    raw_response = @tree.client.send_recv(write_request)
    response = RubySMB::SMB1::Packet::WriteAndxResponse.read(raw_response)
    unless response.valid?
      raise RubySMB::Error::InvalidPacket.new(
        expected_proto: RubySMB::SMB1::SMB_PROTOCOL_ID,
        expected_cmd:   RubySMB::SMB1::Packet::WriteAndxResponse::COMMAND,
        packet:         response
      )
    end
    unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
      raise RubySMB::Error::UnexpectedStatusCode, response.status_code
    end
    bytes_written = response.parameter_block.count_low + (response.parameter_block.count_high << 16)
    total_bytes_written += bytes_written
    offset += bytes_written
    bytes -= bytes_written
    break unless buffer.length > 0
  end

  total_bytes_written
end
write_packet(data:'', offset: 0) click to toggle source

Creates the Request packet for the write command

@param data [String] the data to write to the file @param offset [Integer] the offset in the file to start writing from @return [RubySMB::SMB1::Packet::WriteAndxRequest] the request packet

# File lib/ruby_smb/smb1/file.rb, line 251
def write_packet(data:'', offset: 0)
  write_request = set_header_fields(RubySMB::SMB1::Packet::WriteAndxRequest.new)
  write_request.parameter_block.offset = offset
  write_request.parameter_block.write_mode.writethrough_mode = 1
  write_request.data_block.data = data
  write_request.parameter_block.remaining = write_request.parameter_block.data_length
  write_request
end

Private Instance Methods

set_trans2_params(request) click to toggle source

Sets ParameterBlock options for Trans2 requests

# File lib/ruby_smb/smb1/file.rb, line 320
def set_trans2_params(request)
  request.parameter_block.total_parameter_count = request.parameter_block.parameter_count
  request.parameter_block.total_data_count      = request.parameter_block.data_count
  request.parameter_block.max_parameter_count   = request.parameter_block.parameter_count
  request.parameter_block.max_data_count        = 16_384
  request
end