class RubySMB::SMB2::File

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

Constants

MAX_PACKET_SIZE

The maximum number of byte we want to read or write in a single packet.

Attributes

attributes[RW]

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

@return [RubySMB::Fscc::FileAttributes]
guid[RW]

The {Smb2FileId} for the file @!attribute [rw] guid

@return [RubySMB::Field::Smb2FileId]
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::SMB2::Tree} that this file belong to @!attribute [rw] tree

@return [RubySMB::SMB2::Tree]
tree_connect_encrypt_data[RW]

Whether or not the share associated with this tree connect needs to be encrypted (SMB 3.x) @!attribute [rw] tree_connect_encrypt_data

@return [Boolean]

Public Class Methods

new(tree:, response:, name:, encrypt: false) click to toggle source
# File lib/ruby_smb/smb2/file.rb, line 60
def initialize(tree:, response:, name:, encrypt: false)
  raise ArgumentError, 'No Tree Provided' if tree.nil?
  raise ArgumentError, 'No Response Provided' if response.nil?

  @tree = tree
  @name = name

  @attributes   = response.file_attributes
  @guid         = response.file_id
  @last_access  = response.last_access.to_datetime
  @last_change  = response.last_change.to_datetime
  @last_write   = response.last_write.to_datetime
  @size         = response.end_of_file
  @size_on_disk = response.allocation_size
  @tree_connect_encrypt_data = encrypt
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/smb2/file.rb, line 81
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 is not a CloseResponse packet @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb2/file.rb, line 90
def close
  close_request = set_header_fields(RubySMB::SMB2::Packet::CloseRequest.new)
  raw_response  = tree.client.send_recv(close_request, encrypt: @tree_connect_encrypt_data)
  response = RubySMB::SMB2::Packet::CloseResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::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 is not a SetInfoResponse packet

# File lib/ruby_smb/smb2/file.rb, line 202
def delete
  raw_response = tree.client.send_recv(delete_packet, encrypt: @tree_connect_encrypt_data)
  response = RubySMB::SMB2::Packet::SetInfoResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::Packet::SetInfoResponse::COMMAND,
      packet:         response
    )
  end
  response.smb2_header.nt_status.to_nt_status
end
delete_packet() click to toggle source

Crafts the SetInfoRequest packet to be sent for delete operations.

@return [RubySMB::SMB2::Packet::SetInfoRequest] the set info packet

# File lib/ruby_smb/smb2/file.rb, line 218
def delete_packet
  delete_request                       = set_header_fields(RubySMB::SMB2::Packet::SetInfoRequest.new)
  delete_request.file_info_class       = RubySMB::Fscc::FileInformation::FILE_DISPOSITION_INFORMATION
  delete_request.buffer.delete_pending = 1
  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 is not a ReadResponse packet @raise [RubySMB::Error::UnexpectedStatusCode] if the response NTStatus is not STATUS_SUCCESS

# File lib/ruby_smb/smb2/file.rb, line 116
def read(bytes: size, offset: 0)
  max_read = tree.client.server_max_read_size
  max_read = 65536 unless tree.client.server_supports_multi_credit
  atomic_read_size = [bytes, max_read].min
  credit_charge = 0
  if tree.client.server_supports_multi_credit
    credit_charge = (atomic_read_size - 1) / 65536 + 1
  end

  read_request = read_packet(read_length: atomic_read_size, offset: offset, credit_charge: credit_charge)
  raw_response = tree.client.send_recv(read_request, encrypt: @tree_connect_encrypt_data)
  response     = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end

  data = response.buffer.to_binary_s

  remaining_bytes = bytes - atomic_read_size

  while remaining_bytes > 0
    offset += atomic_read_size
    atomic_read_size = remaining_bytes if remaining_bytes < max_read

    read_request = read_packet(read_length: atomic_read_size, offset: offset, credit_charge: credit_charge)
    raw_response = tree.client.send_recv(read_request, encrypt: @tree_connect_encrypt_data)
    response     = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
    unless response.valid?
      raise RubySMB::Error::InvalidPacket.new(
        expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
        expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
        packet:         response
      )
    end
    unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
      raise RubySMB::Error::UnexpectedStatusCode, response.status_code
    end

    data << response.buffer.to_binary_s
    remaining_bytes -= atomic_read_size
  end
  data
end
read_packet(read_length: 0, offset: 0, credit_charge: 1) 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 @param credit_charge [Integer] the number of credits that this request consumes @return [RubySMB::SMB2::Packet::ReadRequest] the data read from the file

# File lib/ruby_smb/smb2/file.rb, line 173
def read_packet(read_length: 0, offset: 0, credit_charge: 1)
  read_request = set_header_fields(RubySMB::SMB2::Packet::ReadRequest.new)
  read_request.read_length  = read_length
  read_request.offset       = offset
  read_request.smb2_header.credit_charge = credit_charge
  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 is not a SetInfoResponse packet

# File lib/ruby_smb/smb2/file.rb, line 308
def rename(new_file_name)
  raw_response = tree.client.send_recv(rename_packet(new_file_name), encrypt: @tree_connect_encrypt_data)
  response = RubySMB::SMB2::Packet::SetInfoResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::Packet::SetInfoResponse::COMMAND,
      packet:         response
    )
  end
  response.smb2_header.nt_status.to_nt_status
end
rename_packet(new_file_name) click to toggle source

Crafts the SetInfoRequest packet to be sent for rename operations.

@param new_file_name [String] the new name @return [RubySMB::SMB2::Packet::SetInfoRequest] the set info packet

# File lib/ruby_smb/smb2/file.rb, line 325
def rename_packet(new_file_name)
  rename_request                  = set_header_fields(RubySMB::SMB2::Packet::SetInfoRequest.new)
  rename_request.file_info_class  = RubySMB::Fscc::FileInformation::FILE_RENAME_INFORMATION
  rename_request.buffer.file_name = new_file_name.encode('utf-16le')
  rename_request
end
send_recv_read(read_length: 0, offset: 0) click to toggle source
# File lib/ruby_smb/smb2/file.rb, line 181
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, encrypt: @tree_connect_encrypt_data)
  response = RubySMB::SMB2::Packet::ReadResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::Packet::ReadResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end
  response.buffer.to_binary_s
end
send_recv_write(data:'', offset: 0) click to toggle source
# File lib/ruby_smb/smb2/file.rb, line 286
def send_recv_write(data:'', offset: 0)
  pkt = write_packet(data: data, offset: offset)
  raw_response = tree.client.send_recv(pkt, encrypt: @tree_connect_encrypt_data)
  response = RubySMB::SMB2::Packet::WriteResponse.read(raw_response)
  unless response.valid?
    raise RubySMB::Error::InvalidPacket.new(
      expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
      expected_cmd:   RubySMB::SMB2::Packet::WriteResponse::COMMAND,
      packet:         response
    )
  end
  unless response.status_code == WindowsError::NTStatus::STATUS_SUCCESS
    raise RubySMB::Error::UnexpectedStatusCode, response.status_code
  end
  response.write_count
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 rmodified request packet

# File lib/ruby_smb/smb2/file.rb, line 229
def set_header_fields(request)
  request         = tree.set_header_fields(request)
  request.file_id = guid
  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 [WindowsError::ErrorCode] the NTStatus code returned from the operation @raise [RubySMB::Error::InvalidPacket] if the response is not a WriteResponse packet

# File lib/ruby_smb/smb2/file.rb, line 241
def write(data:'', offset: 0)
  max_write = tree.client.server_max_write_size
  max_write = 65536 unless tree.client.server_supports_multi_credit
  buffer            = data.dup
  bytes             = data.length
  atomic_write_size = [bytes, max_write].min
  credit_charge = 0
  if tree.client.server_supports_multi_credit
    credit_charge = (atomic_write_size - 1) / 65536 + 1
  end

  while buffer.length > 0 do
    write_request = write_packet(data: buffer.slice!(0, atomic_write_size), offset: offset, credit_charge: credit_charge)
    raw_response  = tree.client.send_recv(write_request, encrypt: @tree_connect_encrypt_data)
    response      = RubySMB::SMB2::Packet::WriteResponse.read(raw_response)
    unless response.valid?
      raise RubySMB::Error::InvalidPacket.new(
        expected_proto: RubySMB::SMB2::SMB2_PROTOCOL_ID,
        expected_cmd:   RubySMB::SMB2::Packet::WriteResponse::COMMAND,
        packet:         response
      )
    end
    status        = response.smb2_header.nt_status.to_nt_status

    offset += atomic_write_size
    return status unless status == WindowsError::NTStatus::STATUS_SUCCESS
  end

  status
end
write_packet(data:'', offset: 0, credit_charge: 1) 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 @param credit_charge [Integer] the number of credits that this request consumes @return []RubySMB::SMB2::Packet::WriteRequest] the request packet

# File lib/ruby_smb/smb2/file.rb, line 278
def write_packet(data:'', offset: 0, credit_charge: 1)
  write_request               = set_header_fields(RubySMB::SMB2::Packet::WriteRequest.new)
  write_request.write_offset  = offset
  write_request.buffer        = data
  write_request.smb2_header.credit_charge = credit_charge
  write_request
end