ruby_smb 3.1.0 → 3.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/examples/file_server.rb +6 -61
  4. data/examples/virtual_file_server.rb +91 -0
  5. data/lib/ruby_smb/client/negotiation.rb +7 -7
  6. data/lib/ruby_smb/client.rb +2 -2
  7. data/lib/ruby_smb/fscc/file_information/file_access_information.rb +15 -0
  8. data/lib/ruby_smb/fscc/file_information/file_alignment_information.rb +45 -0
  9. data/lib/ruby_smb/fscc/file_information/file_all_information.rb +23 -0
  10. data/lib/ruby_smb/fscc/file_information/file_basic_information.rb +20 -0
  11. data/lib/ruby_smb/fscc/file_information/file_both_directory_information.rb +3 -3
  12. data/lib/ruby_smb/fscc/file_information/file_directory_information.rb +3 -3
  13. data/lib/ruby_smb/fscc/file_information/file_ea_information.rb +1 -0
  14. data/lib/ruby_smb/fscc/file_information/file_full_directory_information.rb +3 -3
  15. data/lib/ruby_smb/fscc/file_information/file_id_both_directory_information.rb +3 -3
  16. data/lib/ruby_smb/fscc/file_information/file_id_full_directory_information.rb +3 -3
  17. data/lib/ruby_smb/fscc/file_information/file_internal_information.rb +15 -0
  18. data/lib/ruby_smb/fscc/file_information/file_mode_information.rb +29 -0
  19. data/lib/ruby_smb/fscc/file_information/file_name_information.rb +16 -0
  20. data/lib/ruby_smb/fscc/file_information/file_names_information.rb +1 -1
  21. data/lib/ruby_smb/fscc/file_information/file_normalized_name_information.rb +16 -0
  22. data/lib/ruby_smb/fscc/file_information/file_position_information.rb +15 -0
  23. data/lib/ruby_smb/fscc/file_information/file_rename_information.rb +1 -1
  24. data/lib/ruby_smb/fscc/file_information/file_standard_information.rb +20 -0
  25. data/lib/ruby_smb/fscc/file_information/file_stream_information.rb +3 -0
  26. data/lib/ruby_smb/fscc/file_information.rb +41 -8
  27. data/lib/ruby_smb/fscc/file_system_information/file_fs_attribute_information.rb +1 -0
  28. data/lib/ruby_smb/fscc/file_system_information/file_fs_volume_information.rb +1 -0
  29. data/lib/ruby_smb/fscc/file_system_information.rb +4 -0
  30. data/lib/ruby_smb/gss/provider/ntlm.rb +20 -3
  31. data/lib/ruby_smb/gss/provider.rb +10 -1
  32. data/lib/ruby_smb/ntlm/client.rb +74 -0
  33. data/lib/ruby_smb/ntlm.rb +1 -0
  34. data/lib/ruby_smb/server/cli.rb +121 -0
  35. data/lib/ruby_smb/server/server_client/session_setup.rb +4 -2
  36. data/lib/ruby_smb/server/server_client.rb +9 -1
  37. data/lib/ruby_smb/server/session.rb +5 -1
  38. data/lib/ruby_smb/server/share/provider/disk/processor/close.rb +9 -5
  39. data/lib/ruby_smb/server/share/provider/disk/processor/create.rb +2 -2
  40. data/lib/ruby_smb/server/share/provider/disk/processor/query.rb +2 -2
  41. data/lib/ruby_smb/server/share/provider/disk/processor/read.rb +15 -14
  42. data/lib/ruby_smb/server/share/provider/disk/processor.rb +76 -12
  43. data/lib/ruby_smb/server/share/provider/disk.rb +8 -2
  44. data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_file.rb +85 -0
  45. data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_pathname.rb +196 -0
  46. data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_stat.rb +175 -0
  47. data/lib/ruby_smb/server/share/provider/virtual_disk.rb +116 -0
  48. data/lib/ruby_smb/server/share/provider.rb +1 -0
  49. data/lib/ruby_smb/server.rb +14 -3
  50. data/lib/ruby_smb/smb2/tree.rb +1 -0
  51. data/lib/ruby_smb/version.rb +1 -1
  52. data/spec/lib/ruby_smb/client_spec.rb +11 -2
  53. data/spec/lib/ruby_smb/fscc/file_information/file_access_information_spec.rb +21 -0
  54. data/spec/lib/ruby_smb/fscc/file_information/file_alignment_information_spec.rb +21 -0
  55. data/spec/lib/ruby_smb/fscc/file_information/file_all_information_spec.rb +61 -0
  56. data/spec/lib/ruby_smb/fscc/file_information/file_basic_information_spec.rb +41 -0
  57. data/spec/lib/ruby_smb/fscc/file_information/file_both_directory_information_spec.rb +59 -10
  58. data/spec/lib/ruby_smb/fscc/file_information/file_directory_information_spec.rb +30 -12
  59. data/spec/lib/ruby_smb/fscc/file_information/file_ea_information_spec.rb +21 -0
  60. data/spec/lib/ruby_smb/fscc/file_information/file_full_directory_information_spec.rb +30 -12
  61. data/spec/lib/ruby_smb/fscc/file_information/file_id_both_directory_information_spec.rb +63 -10
  62. data/spec/lib/ruby_smb/fscc/file_information/file_id_full_directory_information_spec.rb +30 -12
  63. data/spec/lib/ruby_smb/fscc/file_information/file_internal_information_spec.rb +21 -0
  64. data/spec/lib/ruby_smb/fscc/file_information/file_mode_information_spec.rb +21 -0
  65. data/spec/lib/ruby_smb/fscc/file_information/file_name_information_spec.rb +44 -0
  66. data/spec/lib/ruby_smb/fscc/file_information/file_names_information_spec.rb +30 -12
  67. data/spec/lib/ruby_smb/fscc/file_information/file_network_open_information_spec.rb +51 -0
  68. data/spec/lib/ruby_smb/fscc/file_information/file_normalized_name_information_spec.rb +44 -0
  69. data/spec/lib/ruby_smb/fscc/file_information/file_position_information_spec.rb +21 -0
  70. data/spec/lib/ruby_smb/fscc/file_information/file_rename_information_spec.rb +1 -1
  71. data/spec/lib/ruby_smb/fscc/file_information/file_standard_information_spec.rb +41 -0
  72. data/spec/lib/ruby_smb/fscc/file_information/file_stream_information_spec.rb +51 -0
  73. data/spec/lib/ruby_smb/fscc/file_information_spec.rb +14 -0
  74. data/spec/lib/ruby_smb/fscc/file_system_information/file_fs_attribute_information_spec.rb +46 -0
  75. data/spec/lib/ruby_smb/fscc/file_system_information/file_fs_volume_information_spec.rb +51 -0
  76. data/spec/lib/ruby_smb/fscc/file_system_information_spec.rb +14 -0
  77. data/spec/lib/ruby_smb/ntlm/client/session_spec.rb +114 -0
  78. data/spec/lib/ruby_smb/ntlm/client_spec.rb +36 -0
  79. data/spec/lib/ruby_smb/server/server_client_spec.rb +15 -0
  80. data/spec/lib/ruby_smb/server/share/provider/virtual_disk/virtual_pathname_spec.rb +581 -0
  81. data/spec/lib/ruby_smb/server/share/provider/virtual_disk/virtual_stat_spec.rb +207 -0
  82. data/spec/lib/ruby_smb/server/share/provider/virtual_disk_spec.rb +122 -0
  83. data.tar.gz.sig +2 -2
  84. metadata +63 -2
  85. metadata.gz.sig +0 -0
@@ -1,3 +1,4 @@
1
+ require 'zlib'
1
2
  require 'ruby_smb/server/share/provider/processor'
2
3
 
3
4
  module RubySMB
@@ -16,7 +17,7 @@ module RubySMB
16
17
  include RubySMB::Server::Share::Provider::Disk::Processor::Query
17
18
  include RubySMB::Server::Share::Provider::Disk::Processor::Read
18
19
 
19
- Handle = Struct.new(:remote_path, :local_path, :durable?)
20
+ Handle = Struct.new(:remote_path, :local_path, :durable?, :file)
20
21
  def initialize(provider, server_client, session)
21
22
  super
22
23
  @handles = {}
@@ -30,6 +31,25 @@ module RubySMB
30
31
  )
31
32
  end
32
33
 
34
+ # Build an access mask bit field for the specified path. The return type is a DirectoryAccessMask if path
35
+ # is a directory, otherwise it's a FileAccessMask.
36
+ #
37
+ # @param Pathname path the path to build an access mask for
38
+ # @return [DirectoryAccessMask, FileAccessMask] the access mask
39
+ def smb2_access_mask(path)
40
+ # see: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b3af3aaf-9271-4419-b326-eba0341df7d2
41
+ if path.directory?
42
+ am = SMB2::BitField::DirectoryAccessMask.new
43
+ am.traverse = true
44
+ am.list = true
45
+ else
46
+ am = SMB2::BitField::FileAccessMask.new
47
+ am.read_data = true
48
+ end
49
+ am.read_attr = true
50
+ am
51
+ end
52
+
33
53
  private
34
54
 
35
55
  def build_fscc_file_attributes(path)
@@ -44,6 +64,29 @@ module RubySMB
44
64
 
45
65
  def build_fscc_file_information(path, info_class, rename: nil)
46
66
  case info_class
67
+ when Fscc::FileInformation::FILE_ACCESS_INFORMATION
68
+ info = Fscc::FileInformation::FileAccessInformation.new
69
+ # smb2_access_mask returns back either file or directory access mask depending on what path is,
70
+ # FileAccessInformation however isn't defined to account for this context so set it from the binary
71
+ # value
72
+ info.access_flags.read(smb2_access_mask(path).to_binary_s)
73
+ when Fscc::FileInformation::FILE_ALIGNMENT_INFORMATION
74
+ info = Fscc::FileInformation::FileAlignmentInformation.new
75
+ when Fscc::FileInformation::FILE_ALL_INFORMATION
76
+ info = Fscc::FileInformation::FileAllInformation.new
77
+ info.basic_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_BASIC_INFORMATION, rename: rename)
78
+ info.standard_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_STANDARD_INFORMATION, rename: rename)
79
+ info.internal_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_INTERNAL_INFORMATION, rename: rename)
80
+ info.ea_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_EA_INFORMATION, rename: rename)
81
+ info.access_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_ACCESS_INFORMATION, rename: rename)
82
+ info.position_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_POSITION_INFORMATION, rename: rename)
83
+ info.mode_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_MODE_INFORMATION, rename: rename)
84
+ info.alignment_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_ALIGNMENT_INFORMATION, rename: rename)
85
+ info.name_information = build_fscc_file_information(path, Fscc::FileInformation::FILE_NAME_INFORMATION, rename: rename)
86
+ when Fscc::FileInformation::FILE_BASIC_INFORMATION
87
+ info = Fscc::FileInformation::FileBasicInformation.new
88
+ set_common_timestamps(info, path)
89
+ info.file_attributes = build_fscc_file_attributes(path)
47
90
  when Fscc::FileInformation::FILE_EA_INFORMATION
48
91
  info = Fscc::FileInformation::FileEaInformation.new
49
92
  when Fscc::FileInformation::FILE_FULL_DIRECTORY_INFORMATION
@@ -54,21 +97,42 @@ module RubySMB
54
97
  info = Fscc::FileInformation::FileIdBothDirectoryInformation.new
55
98
  set_common_info(info, path)
56
99
  info.file_name = rename || path.basename.to_s
100
+ when Fscc::FileInformation::FILE_ID_FULL_DIRECTORY_INFORMATION
101
+ info = Fscc::FileInformation::FileIdFullDirectoryInformation.new
102
+ set_common_info(info, path)
103
+ info.file_name = rename || path.basename.to_s
104
+ when Fscc::FileInformation::FILE_INTERNAL_INFORMATION
105
+ info = Fscc::FileInformation::FileInternalInformation.new
106
+ info.file_id = Zlib::crc32(path.to_s)
107
+ when Fscc::FileInformation::FILE_MODE_INFORMATION
108
+ info = Fscc::FileInformation::FileModeInformation.new
109
+ when Fscc::FileInformation::FILE_NAME_INFORMATION
110
+ info = Fscc::FileInformation::FileNameInformation.new
111
+ info.file_name = rename || path.basename.to_s
57
112
  when Fscc::FileInformation::FILE_NETWORK_OPEN_INFORMATION
58
113
  info = Fscc::FileInformation::FileNetworkOpenInformation.new
59
114
  set_common_info(info, path)
115
+ when Fscc::FileInformation::FILE_POSITION_INFORMATION
116
+ info = Fscc::FileInformation::FilePositionInformation.new
117
+ info.current_byte_offset = path.size
118
+ when Fscc::FileInformation::FILE_STANDARD_INFORMATION
119
+ info = Fscc::FileInformation::FileStandardInformation.new
120
+ info.allocation_size = get_allocation_size(path)
121
+ info.end_of_file = path.size
122
+ info.directory = path.directory? ? 1 : 0
60
123
  when Fscc::FileInformation::FILE_STREAM_INFORMATION
61
- unless path.file?
62
- raise NotImplementedError, 'Can only generate FILE_STREAM_INFORMATION for files'
63
- end
64
-
65
- info = Fscc::FileInformation::FileStreamInformation.new(
66
- stream_size: path.size,
67
- stream_allocation_size: get_allocation_size(path),
68
- stream_name: '::$DATA'
69
- )
124
+ unless path.file?
125
+ raise NotImplementedError, 'Can only generate FILE_STREAM_INFORMATION for files'
126
+ end
127
+
128
+ info = Fscc::FileInformation::FileStreamInformation.new(
129
+ stream_size: path.size,
130
+ stream_allocation_size: get_allocation_size(path),
131
+ stream_name: '::$DATA'
132
+ )
70
133
  else
71
- raise NotImplementedError, "Unsupported FSCC file information class: #{info_class} (#{Fscc::FileInformation.name(info_class)})"
134
+ logger.warn("Unsupported FSCC file information class: #{info_class} (#{Fscc::FileInformation.name(info_class)})")
135
+ raise NotImplementedError
72
136
  end
73
137
 
74
138
  # some have a next offset field that needs to be set accordingly
@@ -96,7 +160,7 @@ module RubySMB
96
160
  path = path.encode.gsub(/\/|\\/, File::SEPARATOR)
97
161
  path = path.delete_prefix(File::SEPARATOR)
98
162
  local_path = (provider.path + path.encode).cleanpath
99
- unless local_path == provider.path || local_path.to_s.start_with?(provider.path.to_s + '/')
163
+ unless local_path == provider.path || local_path.to_s.start_with?(provider.path.to_s.delete_suffix(File::SEPARATOR) + File::SEPARATOR)
100
164
  raise RuntimeError, "Directory traversal detected to: #{local_path}"
101
165
  end
102
166
  else
@@ -5,14 +5,20 @@ module RubySMB
5
5
  class Server
6
6
  module Share
7
7
  module Provider
8
+ # This is a share provider that exposes the local file system.
8
9
  class Disk < Base
9
10
  TYPE = TYPE_DISK
10
11
  # emulate NTFS just like Samba does
11
12
  FILE_SYSTEM = FileSystem::NTFS
12
13
 
14
+ # @param [String] name The name of this share.
15
+ # @param [String, Pathname] path The local file system path to share. This path must be an absolute path to an existing
16
+ # directory.
13
17
  def initialize(name, path)
14
- path = Pathname.new(File.expand_path(path))
15
- raise ArgumentError unless path.directory?
18
+ path = Pathname.new(File.expand_path(path)) if path.is_a?(String)
19
+ raise ArgumentError.new('path must be a directory') unless path.directory? # it needs to exist
20
+ raise ArgumentError.new('path must be absolute') unless path.absolute? # it needs to be absolute so it is independent of the cwd
21
+
16
22
  @path = path
17
23
  super(name)
18
24
  end
@@ -0,0 +1,85 @@
1
+ require 'ruby_smb/server/share/provider/virtual_disk/virtual_pathname'
2
+ require 'ruby_smb/server/share/provider/virtual_disk/virtual_stat'
3
+
4
+ module RubySMB
5
+ class Server
6
+ module Share
7
+ module Provider
8
+ class VirtualDisk < Disk
9
+ # A dynamic file is one whose contents are generated by the specified
10
+ # block.
11
+ class VirtualDynamicFile < VirtualPathname
12
+ # @param [Hash] disk The mapping of paths to objects representing the virtual file system.
13
+ # @param [String] path The path of this entry.
14
+ # @param [File::Stat] stat An explicit stat object describing the file.
15
+ def initialize(disk, path, stat: nil, &block)
16
+ raise ArgumentError.new('a generation block must be specified') if block.nil?
17
+
18
+ @content_generator = block
19
+ super(disk, path)
20
+ @stat = stat
21
+ end
22
+
23
+ def generate(server_client, session)
24
+ content = @content_generator.call(server_client, session)
25
+ VirtualStaticFile.new(@virtual_disk, @path, content, stat: @stat)
26
+ end
27
+ end
28
+
29
+ # A static file is one whose contents are known at creation time and
30
+ # do not change.
31
+ class VirtualStaticFile < VirtualPathname
32
+ # @param [Hash] disk The mapping of paths to objects representing the virtual file system.
33
+ # @param [String] path The path of this entry.
34
+ # @param [String] content The static content of this file.
35
+ # @param [File::Stat] stat An explicit stat object describing the file.
36
+ def initialize(disk, path, content, stat: nil)
37
+ stat = stat || VirtualStat.new(file?: true, size: content.size)
38
+ raise ArgumentError.new('stat is not a file') unless stat.file?
39
+
40
+ @content = content
41
+ super(disk, path, stat: stat)
42
+ end
43
+
44
+ def open(mode = 'r', &block)
45
+ file = StringIO.new(@content)
46
+ block_given? ? block.call(file) : file
47
+ end
48
+
49
+ attr_reader :content
50
+ end
51
+
52
+ # A mapped file is one who is backed by an entry on disk. The path
53
+ # need not be present, but if it does exist, it must be a file.
54
+ class VirtualMappedFile < VirtualPathname
55
+ # @param [Hash] disk The mapping of paths to objects representing the virtual file system.
56
+ # @param [String] path The path of this entry.
57
+ # @param [String, Pathname] mapped_path The path on the local file system to map into the virtual file system.
58
+ def initialize(disk, path, mapped_path)
59
+ mapped_path = Pathname.new(File.expand_path(mapped_path)) if mapped_path.is_a?(String)
60
+ raise ArgumentError.new('mapped_path must be absolute') unless mapped_path.absolute? # it needs to be absolute so it is independent of the cwd
61
+
62
+ @virtual_disk = disk
63
+ @path = path
64
+ @mapped_path = mapped_path
65
+ end
66
+
67
+ def exist?
68
+ # filter out anything that's not a directory but allow the file to be missing, this prevents exposing
69
+ # directories which could yield path confusion errors
70
+ @mapped_path.exist? && @mapped_path.file?
71
+ end
72
+
73
+ def stat
74
+ @mapped_path.stat
75
+ end
76
+
77
+ def open(mode = 'r', &block)
78
+ @mapped_path.open(mode, &block)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,196 @@
1
+ module RubySMB
2
+ class Server
3
+ module Share
4
+ module Provider
5
+ class VirtualDisk < Disk
6
+ # This object emulates Ruby's builtin Pathname object but uses a virtual file system instead of the real local
7
+ # one.
8
+ class VirtualPathname
9
+ SEPARATOR = File::SEPARATOR
10
+ # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html
11
+ STAT_METHODS = %i[
12
+ atime
13
+ birthtime
14
+ blockdev?
15
+ chardev?
16
+ ctime
17
+ directory?
18
+ executable?
19
+ file?
20
+ ftype
21
+ grpowned?
22
+ mtime
23
+ owned?
24
+ pipe?
25
+ readable?
26
+ setgid?
27
+ setuid?
28
+ size
29
+ socket?
30
+ sticky?
31
+ symlink?
32
+ world_readable?
33
+ world_writable?
34
+ writable?
35
+ zero?
36
+ ]
37
+ private_constant :STAT_METHODS
38
+
39
+ attr_accessor :virtual_disk
40
+
41
+ # @param [Hash] disk The mapping of paths to objects representing the virtual file system.
42
+ # @param [String] path The path of this entry.
43
+ # @param [Boolean] exist? Whether or not this entry represents an existing entry in the virtual file system.
44
+ # @param [File::Stat] stat An explicit stat that represents this object. A default VirtualStat will be
45
+ # created unless specified.
46
+ def initialize(disk, path, **kwargs)
47
+ @virtual_disk = disk
48
+ @path = path
49
+
50
+ if kwargs.fetch(:exist?, true)
51
+ if kwargs[:stat]
52
+ if kwargs[:stat].is_a?(Hash)
53
+ @stat = VirtualStat.new(**kwargs[:stat])
54
+ else
55
+ @stat = kwargs[:stat]
56
+ end
57
+ else
58
+ @stat = VirtualStat.new
59
+ end
60
+ else
61
+ raise ArgumentError.new('can not specify a stat object when exist? is false') if kwargs[:stat]
62
+ @stat = nil
63
+ end
64
+ end
65
+
66
+ def ==(other)
67
+ other.is_a?(self.class) && other.to_s == to_s
68
+ end
69
+
70
+ def <=>(other)
71
+ to_s <=> other.to_s
72
+ end
73
+
74
+ def exist?
75
+ !@stat.nil?
76
+ rescue Errno::ENOENT
77
+ false
78
+ end
79
+
80
+ def stat
81
+ raise Errno::ENOENT.new('No such file or directory') unless exist? && (@stat.file? || @stat.directory?)
82
+
83
+ @stat
84
+ end
85
+
86
+ def join(other)
87
+ # per the docs this Pathname#join doesn't touch the file system
88
+ # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html#class-Pathname-label-Core+methods
89
+ lookup_or_create(Pathname.new(to_s).join(other).to_s)
90
+ end
91
+
92
+ alias :+ :join
93
+ alias :/ :join
94
+
95
+ def to_s
96
+ @path
97
+ end
98
+
99
+ def absolute?
100
+ to_s.start_with?(SEPARATOR)
101
+ end
102
+
103
+ def relative?
104
+ !absolute?
105
+ end
106
+
107
+ def basename
108
+ lookup_or_create(self.class.basename(to_s))
109
+ end
110
+
111
+ def self.basename(*args)
112
+ File.basename(*args)
113
+ end
114
+
115
+ def dirname
116
+ lookup_or_create(self.class.dirname(to_s))
117
+ end
118
+
119
+ def self.dirname(*args)
120
+ File.dirname(*args)
121
+ end
122
+
123
+ def extname
124
+ File.extname(to_s)
125
+ end
126
+
127
+ def split
128
+ [dirname, basename]
129
+ end
130
+
131
+ alias :parent :dirname
132
+
133
+ def children(with_directory=true)
134
+ raise Errno::ENOTDIR.new("Not a directory @ dir_initialize - #{to_s}") unless directory?
135
+
136
+ @virtual_disk.each_value.select { |dent| dent.dirname == self && dent != self }.map { |dent| with_directory ? dent : dent.basename }
137
+ end
138
+
139
+ def entries
140
+ children(false)
141
+ end
142
+
143
+ def cleanpath(consider_symlink=false)
144
+ lookup_or_create(self.class.cleanpath(to_s), stat: (exist? ? stat : nil))
145
+ end
146
+
147
+ def self.cleanpath(path)
148
+ # per the docs this Pathname#cleanpath doesn't touch the file system
149
+ # see: https://ruby-doc.org/stdlib-3.1.1/libdoc/pathname/rdoc/Pathname.html#class-Pathname-label-Core+methods
150
+ Pathname.new(path).cleanpath.to_s
151
+ end
152
+
153
+ private
154
+
155
+ # Check the virtual file system to see if the entry exists. Return it if it does, otherwise create a new
156
+ # entry representing a non-existent path.
157
+ #
158
+ # @param [String] path The path to lookup in the virtual file system. It will be normalized using #cleanpath.
159
+ # @return [Pathname] The path object representing the specified string.
160
+ def lookup_or_create(path, **kwargs)
161
+ existing = @virtual_disk[self.class.cleanpath(path)]
162
+ return existing if existing
163
+
164
+ kwargs[:exist?] = false
165
+ VirtualPathname.new(@virtual_disk, path, **kwargs)
166
+ end
167
+
168
+ def method_missing(symbol, *args)
169
+ # should we forward to one of the stat methods
170
+ if STAT_METHODS.include?(symbol)
171
+ # if we have a stat object then forward it
172
+ return stat.send(symbol, *args) if exist?
173
+ # if we don't have a stat object, emulate what Pathname does when it does not exist
174
+
175
+ # these two methods return nil
176
+ return nil if %i[ world_readable? world_writable? ].include?(symbol)
177
+
178
+ # any of the other ?-suffixed methods return false
179
+ return false if symbol.to_s.end_with?('?')
180
+
181
+ # any other method raises a Errno::ENOENT exception
182
+ raise Errno::ENOENT.new('No such file or directory')
183
+ end
184
+
185
+ raise NoMethodError, "undefined method `#{symbol}' for #{self.class}"
186
+ end
187
+
188
+ def respond_to_missing?(symbol, include_private = false)
189
+ STAT_METHODS.include?(symbol)
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,175 @@
1
+ module RubySMB
2
+ class Server
3
+ module Share
4
+ module Provider
5
+ class VirtualDisk < Disk
6
+ # This object emulates Ruby's builtin File::Stat object but uses a virtual file system instead of the real
7
+ # local one. The current implementation is limited to only representing directories and standard files. All
8
+ # attributes are read-only and once the object is created, it is immutable.
9
+ class VirtualStat
10
+
11
+ # All of the keyword arguments are the keys of the attributes to set. The names are left as is, maintaining
12
+ # a direct 1 to 1 relationship. See the Ruby docs for File::Stat
13
+ # (https://ruby-doc.org/core-3.0.2/File/Stat.html) for a list of all the attributes that can be set. Some
14
+ # values are calculated based on others such as the mode readable? being calculated based on the mode.
15
+ def initialize(**kwargs)
16
+ # directory and file both default to being the opposite of each other, one or both can be specified
17
+ # but they can not both be true at the same time
18
+ is_dir = !!kwargs.fetch(:directory?, !kwargs.fetch(:file?, false)) # defaults to not file which defaults to false
19
+ is_fil = !!kwargs.fetch(:file?, !kwargs.fetch(:directory?, true)) # defaults to not directory which defaults to true
20
+ raise ArgumentError.new('must be either a file or a directory') unless is_dir ^ is_fil
21
+
22
+ @values = kwargs.dup
23
+ # the default is a directory
24
+ @values[:directory?] = !@values.delete(:file?) if @values.key?(:file?) # normalize on directory? if file? was specified.
25
+
26
+ @birthtime = kwargs[:birthtime] || Time.now
27
+ end
28
+
29
+ def blksize
30
+ @values.fetch(:blksize, 4096)
31
+ end
32
+
33
+ def blockdev?
34
+ false
35
+ end
36
+
37
+ def blocks
38
+ @values.fetch(:blocks, 0)
39
+ end
40
+
41
+ def chardev?
42
+ false
43
+ end
44
+
45
+ def pipe?
46
+ false
47
+ end
48
+
49
+ def socket?
50
+ false
51
+ end
52
+
53
+ def symlink?
54
+ false
55
+ end
56
+
57
+ def directory?
58
+ @values.fetch(:directory?, true)
59
+ end
60
+
61
+ def file?
62
+ !directory?
63
+ end
64
+
65
+ def ftype
66
+ raise Errno::ENOENT.new('No such file or directory') unless file? || directory?
67
+
68
+ file? ? 'file' : 'directory'
69
+ end
70
+
71
+ def size
72
+ @values.fetch(:size, 0)
73
+ end
74
+
75
+ def zero?
76
+ file? && size == 0
77
+ end
78
+
79
+ def nlink
80
+ @values.fetch(:nlink, 0)
81
+ end
82
+
83
+ def dev
84
+ @values[:dev] ||= rand(1..0xfe)
85
+ end
86
+
87
+ def ino
88
+ @values[:ino] ||= rand(1..0xfffe)
89
+ end
90
+
91
+ def gid
92
+ @values.fetch(:gid, Process.gid)
93
+ end
94
+
95
+ def grpowned?
96
+ gid == Process.gid
97
+ end
98
+
99
+ def uid
100
+ @values.fetch(:uid, Process.uid)
101
+ end
102
+
103
+ def owned?
104
+ uid == Process.uid
105
+ end
106
+
107
+ # last access time
108
+ def atime
109
+ @values.fetch(:atime, @birthtime)
110
+ end
111
+
112
+ # modification time
113
+ def mtime
114
+ @values.fetch(:mtime, @birthtime)
115
+ end
116
+
117
+ # change time
118
+ def ctime
119
+ @values.fetch(:ctime, @birthtime)
120
+ end
121
+
122
+ # the permission bits, normalized based on the standard GNU representation,
123
+ # see: https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html
124
+ def mode
125
+ @values.fetch(:mode, (file? ? 0o644 : 0o755))
126
+ end
127
+
128
+ def setuid?
129
+ mode & 0o04000 != 0
130
+ end
131
+
132
+ def setgid?
133
+ mode & 0o02000 != 0
134
+ end
135
+
136
+ def sticky?
137
+ mode & 0o01000 != 0
138
+ end
139
+
140
+ def readable?
141
+ return true if owned? && (mode & 1 << 8 != 0)
142
+ return true if grpowned? && (mode & 1 << 5 != 0)
143
+ return true if world_readable?
144
+ return false
145
+ end
146
+
147
+ def world_readable?
148
+ mode & 1 << 2 != 0
149
+ end
150
+
151
+ def writable?
152
+ return true if owned? && (mode & 1 << 7 != 0)
153
+ return true if grpowned? && (mode & 1 << 4 != 0)
154
+ return true if world_writable?
155
+ return false
156
+ end
157
+
158
+ def world_writable?
159
+ mode & 1 << 1 != 0
160
+ end
161
+
162
+ def executable?
163
+ return true if owned? && (mode & 1 << 6 != 0)
164
+ return true if grpowned? && (mode & 1 << 3 != 0)
165
+ return true if mode & 1 != 0
166
+ return false
167
+ end
168
+
169
+ attr_reader :birthtime
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end