ruby_smb 3.1.0 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
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