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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/examples/file_server.rb +6 -61
- data/examples/virtual_file_server.rb +91 -0
- data/lib/ruby_smb/client/negotiation.rb +7 -7
- data/lib/ruby_smb/client.rb +2 -2
- data/lib/ruby_smb/fscc/file_information/file_access_information.rb +15 -0
- data/lib/ruby_smb/fscc/file_information/file_alignment_information.rb +45 -0
- data/lib/ruby_smb/fscc/file_information/file_all_information.rb +23 -0
- data/lib/ruby_smb/fscc/file_information/file_basic_information.rb +20 -0
- data/lib/ruby_smb/fscc/file_information/file_both_directory_information.rb +3 -3
- data/lib/ruby_smb/fscc/file_information/file_directory_information.rb +3 -3
- data/lib/ruby_smb/fscc/file_information/file_ea_information.rb +1 -0
- data/lib/ruby_smb/fscc/file_information/file_full_directory_information.rb +3 -3
- data/lib/ruby_smb/fscc/file_information/file_id_both_directory_information.rb +3 -3
- data/lib/ruby_smb/fscc/file_information/file_id_full_directory_information.rb +3 -3
- data/lib/ruby_smb/fscc/file_information/file_internal_information.rb +15 -0
- data/lib/ruby_smb/fscc/file_information/file_mode_information.rb +29 -0
- data/lib/ruby_smb/fscc/file_information/file_name_information.rb +16 -0
- data/lib/ruby_smb/fscc/file_information/file_names_information.rb +1 -1
- data/lib/ruby_smb/fscc/file_information/file_normalized_name_information.rb +16 -0
- data/lib/ruby_smb/fscc/file_information/file_position_information.rb +15 -0
- data/lib/ruby_smb/fscc/file_information/file_rename_information.rb +1 -1
- data/lib/ruby_smb/fscc/file_information/file_standard_information.rb +20 -0
- data/lib/ruby_smb/fscc/file_information/file_stream_information.rb +3 -0
- data/lib/ruby_smb/fscc/file_information.rb +41 -8
- data/lib/ruby_smb/fscc/file_system_information/file_fs_attribute_information.rb +1 -0
- data/lib/ruby_smb/fscc/file_system_information/file_fs_volume_information.rb +1 -0
- data/lib/ruby_smb/fscc/file_system_information.rb +4 -0
- data/lib/ruby_smb/gss/provider/ntlm.rb +20 -3
- data/lib/ruby_smb/gss/provider.rb +10 -1
- data/lib/ruby_smb/ntlm/client.rb +74 -0
- data/lib/ruby_smb/ntlm.rb +1 -0
- data/lib/ruby_smb/server/cli.rb +121 -0
- data/lib/ruby_smb/server/server_client/session_setup.rb +4 -2
- data/lib/ruby_smb/server/server_client.rb +9 -1
- data/lib/ruby_smb/server/session.rb +5 -1
- data/lib/ruby_smb/server/share/provider/disk/processor/close.rb +9 -5
- data/lib/ruby_smb/server/share/provider/disk/processor/create.rb +2 -2
- data/lib/ruby_smb/server/share/provider/disk/processor/query.rb +2 -2
- data/lib/ruby_smb/server/share/provider/disk/processor/read.rb +15 -14
- data/lib/ruby_smb/server/share/provider/disk/processor.rb +76 -12
- data/lib/ruby_smb/server/share/provider/disk.rb +8 -2
- data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_file.rb +85 -0
- data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_pathname.rb +196 -0
- data/lib/ruby_smb/server/share/provider/virtual_disk/virtual_stat.rb +175 -0
- data/lib/ruby_smb/server/share/provider/virtual_disk.rb +116 -0
- data/lib/ruby_smb/server/share/provider.rb +1 -0
- data/lib/ruby_smb/server.rb +14 -3
- data/lib/ruby_smb/smb2/tree.rb +1 -0
- data/lib/ruby_smb/version.rb +1 -1
- data/spec/lib/ruby_smb/client_spec.rb +11 -2
- data/spec/lib/ruby_smb/fscc/file_information/file_access_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_alignment_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_all_information_spec.rb +61 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_basic_information_spec.rb +41 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_both_directory_information_spec.rb +59 -10
- data/spec/lib/ruby_smb/fscc/file_information/file_directory_information_spec.rb +30 -12
- data/spec/lib/ruby_smb/fscc/file_information/file_ea_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_full_directory_information_spec.rb +30 -12
- data/spec/lib/ruby_smb/fscc/file_information/file_id_both_directory_information_spec.rb +63 -10
- data/spec/lib/ruby_smb/fscc/file_information/file_id_full_directory_information_spec.rb +30 -12
- data/spec/lib/ruby_smb/fscc/file_information/file_internal_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_mode_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_name_information_spec.rb +44 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_names_information_spec.rb +30 -12
- data/spec/lib/ruby_smb/fscc/file_information/file_network_open_information_spec.rb +51 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_normalized_name_information_spec.rb +44 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_position_information_spec.rb +21 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_rename_information_spec.rb +1 -1
- data/spec/lib/ruby_smb/fscc/file_information/file_standard_information_spec.rb +41 -0
- data/spec/lib/ruby_smb/fscc/file_information/file_stream_information_spec.rb +51 -0
- data/spec/lib/ruby_smb/fscc/file_information_spec.rb +14 -0
- data/spec/lib/ruby_smb/fscc/file_system_information/file_fs_attribute_information_spec.rb +46 -0
- data/spec/lib/ruby_smb/fscc/file_system_information/file_fs_volume_information_spec.rb +51 -0
- data/spec/lib/ruby_smb/fscc/file_system_information_spec.rb +14 -0
- data/spec/lib/ruby_smb/ntlm/client/session_spec.rb +114 -0
- data/spec/lib/ruby_smb/ntlm/client_spec.rb +36 -0
- data/spec/lib/ruby_smb/server/server_client_spec.rb +15 -0
- data/spec/lib/ruby_smb/server/share/provider/virtual_disk/virtual_pathname_spec.rb +581 -0
- data/spec/lib/ruby_smb/server/share/provider/virtual_disk/virtual_stat_spec.rb +207 -0
- data/spec/lib/ruby_smb/server/share/provider/virtual_disk_spec.rb +122 -0
- data.tar.gz.sig +2 -2
- metadata +63 -2
- 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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|