homefs 0.1.1 → 0.2.0

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 (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/homefs/homefs.rb +283 -37
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7c2589a5398a08b340c286465ef325c6a5cd479d
4
- data.tar.gz: e38703f8191b106040c3d24820662264b46543ed
3
+ metadata.gz: e74057ef8ec81a7efa25cb238fa1d960ec7fa727
4
+ data.tar.gz: 6a9bff9e3760f6d4f35f5a368849e17d7caecb07
5
5
  SHA512:
6
- metadata.gz: 1b24049d47a6c5c35bc69305edb6eb9a6b4306b491187beafa492cdfef44f793c11222dc6fb7edb9a517f01a4ac6d3ab2d63baf2cc3767493098770ba6de6115
7
- data.tar.gz: 6a75b18765bd4f35f9a464e0b15bf38418814978df1b409d75b15db78280dda08f987f32a43d955ee83c3f9c69bdf713ed58268098c2272b3995321b907512d8
6
+ metadata.gz: 60a6183228c6436b89a30581a85e47211b9bcf1a56f6fe4e0c50d63b30929c1aa456266be3e2dfde148cec82dbbe062131f57be173d2875c149125e6f83f42cf
7
+ data.tar.gz: 0a58275485f48cafdc452161d78cdd030b57e9965b22c15608c443db60842386cbdfa4b5d164e0751387cb5f0d0752689af2a634db8e12bff09c34d94603b061
data/lib/homefs/homefs.rb CHANGED
@@ -1,25 +1,89 @@
1
1
  require 'rfusefs'
2
2
 
3
+ # An instance of HomeFS; this implements all FuseFS methods, and ultimately
4
+ # handles reading, writing, and inspection of files.
5
+ #
6
+ # We rely on the context provided to us by FUSE to determine the calling user's
7
+ # UID and GID. If you want to access an instance of this class directly, you may
8
+ # set the UID and GID of the current thread like so
9
+ # Thread.current[:fusefs_reader_uid] = uid
10
+ # Thread.current[:fusefs_reader_gid] = gid
11
+ # where 'uid' and 'gid' are the desired UID and GID you wish to emulate.
12
+ # @author Dylan Frese
3
13
  class HomeFS
14
+
15
+ # Creates a new instance of HomeFS. This instance should probably be passed
16
+ # to FuseFS, as it does nothing on its own. The path is relative to the
17
+ # calling user's home directory, that is, the user interacting with the
18
+ # filesystem through open(2), read(2), write(2), etc.
19
+ # @param [String] path the path, relative to $HOME, to use as the root of
20
+ # the file system.
4
21
  def initialize(path)
5
22
  @relpath = path
6
- @dirlinks = Hash.new
7
23
  read_passwd
24
+ read_group
8
25
  end
9
26
 
27
+ # Read or re-read /etc/passwd to update the internal table of home
28
+ # directories for each UID. This method does not try to guess as to which
29
+ # user IDs are owned by actual users of the system, and which are utility
30
+ # accounts such as 'lp' or 'pulse'.
31
+ # @return [void]
10
32
  def read_passwd
11
- # Here we map each line in /etc/passwd to an array,
33
+ # Try to read from getent
34
+ passwd = `getent passwd`
35
+ if $? != 0
36
+ passwd = File.read('/etc/passwd')
37
+ end
38
+
39
+ @homedirs = Hash.new
40
+ @usernames = Hash.new
41
+
42
+ # Here we map each line in passwd to an array,
12
43
  # which Hash interprets as a key, value pair.
13
- @homedirs = Hash[
14
- File.readlines('/etc/passwd').map do |line|
15
- next if line.strip.empty?
16
- values = line.split(':')
17
- # UID, home directory
18
- [Integer(values[2]), values[5]]
19
- end
20
- ]
44
+ passwd.each_line.map { |line|
45
+ next if line.strip.empty?
46
+ values = line.split(':')
47
+ # Username, UID, home directory
48
+ [values[0], Integer(values[2]), values[5]]
49
+ }.reject { |line|
50
+ line.nil?
51
+ }.each { |username, uid, home|
52
+ @homedirs[uid] = home
53
+ @usernames[username] = uid
54
+ }
55
+ end
56
+
57
+ # Read or re-read /etc/group to update the internal table of group
58
+ # group membership.
59
+ # @return [void]
60
+ def read_group
61
+ # Try to read from getent
62
+ group = `getent group`
63
+ if $? != 0
64
+ group = File.read('/etc/group')
65
+ end
66
+
67
+ @groups = Hash.new
68
+
69
+ group.each_line.map { |line|
70
+ next if line.strip.empty?
71
+ _name, _, gid, members = line.split(':')
72
+ members = members.strip.split(',').map {|member| p member; @usernames[member]}
73
+ [Integer(gid), members]
74
+ }.reject { |line|
75
+ line.nil?
76
+ }.each { |gid, members|
77
+ @groups[gid] = members
78
+ }
21
79
  end
22
80
 
81
+ # Return the path to the root of HomeFS relative to the underlying
82
+ # filesystem. If _path_ is specified, it is taken to be relative to the
83
+ # root of HomeFS.
84
+ # @return [String] path to the root of the HomeFS relative to the underlying
85
+ # filesystem, or if _path_ is specified, the path to that resource
86
+ # relative to the underlying filesystem.
23
87
  def homepath(path = nil)
24
88
  uid = FuseFS.reader_uid
25
89
  basepath = @homedirs[uid]
@@ -27,85 +91,184 @@ class HomeFS
27
91
  # in case.
28
92
  raise Errno::ENOENT if basepath == nil
29
93
  if path
30
- "#{basepath}/#{@relpath}/#{path}"
94
+ File.join(basepath, @relpath, path)
31
95
  else
32
- "#{basepath}/#{@relpath}"
96
+ File.join(basepath, @relpath)
33
97
  end
34
98
  end
35
99
 
100
+ # Check whether the given file has a mode that matches the given mask. If
101
+ # check_ids is true, the third-least significant digit of the mask is set to
102
+ # zero if the calling user is not the owner of the file, and the
103
+ # second-least significant digit of the mask is zeroed if the calling user's
104
+ # group is not the group of the file. This has the effect of only checking
105
+ # the owner's permissions if we're the owner, and only checking the group
106
+ # permissions if we're in that group.
107
+ #
108
+ # This method checks if _any_ of the set bits in the mask are set in the
109
+ # file's mode. It does not check if the mask matches the file's mode, nor
110
+ # does it check if _all_ set bits in the mask are set in the file's mode.
111
+ #
112
+ # The first three least significant digits of the mode, in octal, (that is,
113
+ # the three rightmost digits) control the read (4), write (2), and execute
114
+ # (1) permissions of the file. The next digit controls setuid (4), setgid
115
+ # (2), and the sticky bit (1) of the file.
116
+ #
117
+ # It is recommended to see
118
+ # man chmod
119
+ # to get a more detailed description about file modes.
120
+ #
121
+ # @example Test if we can write to a file
122
+ # mode_mask("/example", 0222, true)
123
+ # mode_mask("/example", 0222)
124
+ # @example Test if a file is executable by anyone
125
+ # mode_mask("/binary", 0111, false)
126
+ # @example Test if a directory has the sticky bit set
127
+ # mode_mask("/tmp", 01000, false)
128
+ #
129
+ # @param [String] file the path to the file to check
130
+ # @param [Integer] mask the mask against which to check the file's mode.
131
+ # It is recommended you write this in octal (with a leading 0).
132
+ # @param [Boolean] check_ids if true, only check user and group permissions
133
+ # if the caller is, respectively, the owner of the file/in the file's
134
+ # group.
135
+ # @return [Boolean] whether the mode of _file_ matches the given mask
36
136
  def mode_mask(file, mask, check_ids = true)
37
137
  stat = File.stat(file)
38
- # First digit: setuid (4), setgid (2), sticky bit (1)
39
- # Second, third, and fourth digits: user, group, and
40
- # world permissions for read (4), write (2), execute (1)
41
- # (see `man chmod` for more)
42
138
  fmode = stat.mode
43
139
  if check_ids
44
140
  fuid, fgid = stat.uid, stat.gid
45
- uid, gid = FuseFS.reader_uid, FuseFS.reader_gid
141
+ uid = FuseFS.reader_uid
46
142
  # Zero out the third digit (in octal).
47
143
  # We could use a constant here, but this works
48
144
  # for a mask of any length
49
145
  if uid != fuid
50
146
  mask &= ~(mask & 0700)
51
147
  end
52
- if gid != fgid
148
+ if !@groups[fgid].include?(uid)
53
149
  mask &= ~(mask & 0070)
54
150
  end
55
151
  end
56
152
  fmode & mask != 0
57
153
  end
58
154
 
155
+ # Test whether we can write to the given path. If no file exists at _path_,
156
+ # we test if the parent directory is either writable or has the sticky bit
157
+ # set.
158
+ # @param [String] path the path to test
159
+ # @return [Boolean] whether the given path is writable
160
+ def writable?(path)
161
+ return true if mode_mask(path, 0222)
162
+ if !File.exist?(path)
163
+ mode_mask(File.dirname(path), 01222)
164
+ else
165
+ false
166
+ end
167
+ end
168
+
169
+ # Rename the file or directory at from_path to to_path. This is mostly
170
+ # equivalent to /bin/mv.
171
+ # @param [String] from_path the path of the file to be moved
172
+ # @param [String] to_path the destination of the file
173
+ # @return [void]
174
+ # @note This method should usually only be called by FUSE, and not called
175
+ # directly
59
176
  def rename(from_path, to_path)
60
177
  FileUtils.mv(homepath(from_path), homepath(to_path))
61
178
  end
62
179
 
180
+ # Update the mtime and atime (last-modified and last-accessed time) of the
181
+ # file at _path_ to the time given by modtime.
182
+ # @param [String] path the path to the file to touch
183
+ # @param [Time] modtime the time to update the file's times to
184
+ # @return [void]
185
+ # @note This method should usually only be called by FUSE, and not called
186
+ # directly
63
187
  def touch(path, modtime)
64
188
  File.utime(modtime, modtime, homepath(path))
65
189
  end
66
190
 
191
+ # Remove the directory at the given path. The directory must be empty. This
192
+ # is mostly equivalent to rmdir(1).
193
+ # @param [String] path the path to the directory to remove
194
+ # @return [void]
195
+ # @note This method should usually only be called by FUSE, and not called
196
+ # directly
67
197
  def rmdir(path)
68
198
  FileUtils.rmdir(homepath(path))
69
199
  end
70
200
 
201
+ # Test whether we can remove the directory given by _path_.
202
+ # @param [String] path the path to the directory to test
203
+ # @return [Boolean] true if the directory given can be removed.
204
+ # @note This method should usually only be called by FUSE, and not called
205
+ # directly
71
206
  def can_rmdir?(path)
72
- File.writable?(homepath(path))
207
+ File.writable?(homepath(path)) &&
208
+ Dir.new(path).to_a.size == 2
73
209
  end
74
210
 
211
+ # Make a new directory at _path_. This is similar to mkdir(1).
212
+ # @param [String] path the path to the directory to make
213
+ # @return [void]
214
+ # @note This method should usually only be called by FUSE, and not called
215
+ # directly
75
216
  def mkdir(path)
76
217
  FileUtils.mkdir(homepath(path))
77
218
  end
78
219
 
220
+ # Test if we can make a directory at the given path.
221
+ # @param [String] path the path to test
222
+ # @return [Boolean] true if we can make a directory at _path_.
223
+ # @note This method should usually only be called by FUSE, and not called
224
+ # directly
79
225
  def can_mkdir?(path)
80
- writable?(File.dirname(homepath(path)))
226
+ writable?(homepath(path))
81
227
  end
82
228
 
229
+ # Delete the file given by _path_. This is similar to rm(1).
230
+ # @param [String] path the path to the file to delete
231
+ # @return [void]
232
+ # @note This method should usually only be called by FUSE, and not called
233
+ # directly
83
234
  def delete(path)
84
235
  FileUtils.rm(homepath(path))
85
236
  end
86
237
 
238
+ # Test if we can delete the file given by _path_.
239
+ # @param [String] path the path to the file to test
240
+ # @return [void]
241
+ # @note This method should usually only be called by FUSE, and not called
242
+ # directly
87
243
  def can_delete?(path)
88
244
  writable?(File.dirname(homepath(path)))
89
245
  end
90
246
 
247
+ # Write the given data to the given path.
248
+ # @param [String] path the path to write to
249
+ # @param [String] str a binary string of data to write
250
+ # @return [void]
251
+ # @note This method should usually only be called by FUSE, and not called
252
+ # directly
91
253
  def write_to(path, str)
92
254
  File.open(homepath(path), "wb") {|file| file.write(str) }
93
255
  end
94
256
 
95
- def writable?(path)
96
- mode_mask(path, 0222)
97
- end
98
-
257
+ # Test whether we can write out to the given path.
258
+ # @param [String] path the path to test
259
+ # @return [Boolean] whether we can write to the given path
260
+ # @note This method should usually only be called by FUSE, and not called
261
+ # directly
99
262
  def can_write?(path)
100
- hpath = homepath(path)
101
- if File.exist?(hpath)
102
- writable?(hpath)
103
- else
104
- # FIXME: We don't support sticky bits
105
- writable?(File.dirname(hpath))
106
- end
263
+ writable?(homepath(path))
107
264
  end
108
265
 
266
+ # Get the times (atime, mtime, ctime) for the file at the given path.
267
+ # @param [String] path the path to the file to get the times for
268
+ # @return [Array<Time>] an array of size three of the last-accessed time,
269
+ # the last-modified time, and the creation time for the file
270
+ # @note This method should usually only be called by FUSE, and not called
271
+ # directly
109
272
  def times(path)
110
273
  atime = File.atime(homepath(path))
111
274
  mtime = File.mtime(homepath(path))
@@ -113,32 +276,73 @@ class HomeFS
113
276
  [atime, mtime, ctime]
114
277
  end
115
278
 
279
+ # Test whether the file given by _path_ is executable.
280
+ # @param [String] path the path to the file to test
281
+ # @return [Boolean] whether the file at the given path is executable.
282
+ # @note This method should usually only be called by FUSE, and not called
283
+ # directly
116
284
  def executable?(path)
117
285
  mode_mask(homepath(path), 0111)
118
286
  end
119
287
 
288
+ # Read the contents of the file given by _path_.
289
+ # @param [String] path the path of the file to read
290
+ # @return [String] a binary string of the contents of the file
291
+ # @note This method should usually only be called by FUSE, and not called
292
+ # directly
120
293
  def read_file(path)
121
294
  File.open(homepath(path), "rb") do |file|
122
295
  file.read
123
296
  end
124
297
  end
125
298
 
299
+ # List the contents of a directory. This includes '.' and '..' (the current
300
+ # directory and its parent).
301
+ # @param [String] path the path to the directory to list
302
+ # @return [Array<String>] a list of file names of files in the directory
303
+ # @note This method should usually only be called by FUSE, and not called
304
+ # directly
126
305
  def contents(path)
127
306
  Dir.new(homepath(path)).to_a
128
307
  end
129
308
 
309
+ # Get the size of the file at _path_
310
+ # @param [String] path the path to the file or directory to get the size of
311
+ # @return [Integer] the size, in bytes, of the file
312
+ # @note This method should usually only be called by FUSE, and not called
313
+ # directly
130
314
  def size(path)
131
315
  File.size(homepath(path))
132
316
  end
133
317
 
318
+ # Test whether _path_ is the name of a file (i.e., not a directory)
319
+ # @param [String] path the path to test
320
+ # @return [Boolean] whether _path_ represents a file
321
+ # @note This method should usually only be called by FUSE, and not called
322
+ # directly
134
323
  def file?(path)
135
324
  File.file?(homepath(path))
136
325
  end
137
326
 
327
+ # Test whether _path_ is the name of a directory
328
+ # @param [String] path the path to test
329
+ # @return [Boolean] whether _path_ represents a directory
330
+ # @note This method should usually only be called by FUSE, and not called
331
+ # directly
138
332
  def directory?(path)
139
333
  File.directory?(homepath(path))
140
334
  end
141
335
 
336
+ # Open a file handle to the file given by _path_, with the mode given by
337
+ # _mode_. Right now, the supported modes are "rw", "w", and "r", for
338
+ # read/write, write, and read. If the mode is write or read/write and no
339
+ # file exists at _path_, on will be created.
340
+ # @param [String] path the path to the file to open
341
+ # @param [String] mode the mode to open the file with
342
+ # @param rfusefs ignored
343
+ # @return [File] a file handle of the file given by _path_
344
+ # @note This method should usually only be called by FUSE, and not called
345
+ # directly
142
346
  def raw_open(path, mode, rfusefs = nil)
143
347
  mode = case mode
144
348
  when "rw" then File::RDWR | File::CREAT | File::BINARY
@@ -148,25 +352,67 @@ class HomeFS
148
352
  File.open(homepath(path), mode)
149
353
  end
150
354
 
355
+ # Read _size_ bytes, starting at _offset_, from the file handle given by _raw_
356
+ # (which is returned by a call to _raw_open_). If raw is nil and a path is
357
+ # given, a file will be opened at _path_, and closed after the data is read.
358
+ # Otherwise, _path_ is ignored.
359
+ # @param [String] path the path to the file
360
+ # @param [Integer] offset the offset, in bytes, from the start of the file
361
+ # to start reading from.
362
+ # @param [Integer] size the amount of bytes to read
363
+ # @param [File] raw the file handle to read from
364
+ # @return [String] a binary string of the requested data
365
+ # @note This method should usually only be called by FUSE, and not called
366
+ # directly
151
367
  def raw_read(path, offset, size, raw = nil)
152
- raw ||= raw_open(path, "r")
153
- raw.seek(offset, :SET)
154
- raw.read(size)
368
+ file = raw || raw_open(path, "r")
369
+ file.seek(offset, :SET)
370
+ file.read(size)
371
+ ensure
372
+ file.close if raw.nil?
155
373
  end
156
374
 
375
+ # Sync writes to the underlying filesystem. If _raw_ is nil, no operation is
376
+ # performed.
377
+ # @param path ignored
378
+ # @param datasync ignored
379
+ # @param [File] raw the file handle to sync
380
+ # @return [void]
381
+ # @note This method should usually only be called by FUSE, and not called
382
+ # directly
157
383
  def raw_sync(path, datasync, raw = nil)
158
384
  return if raw.nil? # Should we sync anyway?
159
385
  raw.fdatasync
160
386
  end
161
387
 
388
+ # Close the file handle given by _raw_.
389
+ # @param path ignored
390
+ # @param [File] raw the file handle to close
391
+ # @return [void]
392
+ # @note This method should usually only be called by FUSE, and not called
393
+ # directly
162
394
  def raw_close(path, raw = nil)
163
395
  return if raw.nil? # ???
164
396
  raw.close
165
397
  end
166
398
 
399
+ # Write _sz_ bytes from _buf_ to the file handle _raw_, starting at _off_.
400
+ # If no file handle is given, one will be opened at the given path, and
401
+ # closed after the write is complete.
402
+ # @param [String] path the path of the file
403
+ # @param [Integer] off the offset, in bytes, to starting writing to the
404
+ # file at
405
+ # @param [Integer] sz the amount of bytes to read from buf
406
+ # @param [String] buf a binary string containing the data to be written
407
+ # @param [File] raw the file handle to write to
408
+ # @return [void]
409
+ # @note This method should usually only be called by FUSE, and not called
410
+ # directly
167
411
  def raw_write(path, off, sz, buf, raw = nil)
168
- raw ||= File.open(path, "w")
169
- raw.seek(off, :SET)
170
- raw.write(buf[0...sz])
412
+ file = raw || File.open(path, "w")
413
+ file.seek(off, :SET)
414
+ file.write(buf[0...sz])
415
+ ensure
416
+ file.close if raw.nil?
171
417
  end
172
418
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: homefs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dylan Frese