homefs 0.3.0 → 0.3.1
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
- data/lib/homefs/debug.rb +4 -0
- data/lib/homefs/homefs.rb +110 -255
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9aa5fb16b6856552286724fd6fd1e45f46bc5a6f
|
4
|
+
data.tar.gz: ca26c140a4ad789d383edbe43a7040d2fa23b8f4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7294eef3cd9569a078789e8b148de97054584084e792deb5bee24d5c5d234314fc7ea6d0d7dc11bc69d93002ec1e34cf9e455edc54b5af75e9428e1517e87349
|
7
|
+
data.tar.gz: 5b350ef36937e79201cf61c005acb21447ad014f51c96a1dd06f52753402e19d189e083f339fec10e1536e038bc41a257452878bc52f494ba6bbb1166ab75ddd
|
data/lib/homefs/debug.rb
CHANGED
data/lib/homefs/homefs.rb
CHANGED
@@ -22,7 +22,6 @@ class HomeFS
|
|
22
22
|
def initialize(path, options = Hash.new)
|
23
23
|
@relpath = path
|
24
24
|
read_passwd
|
25
|
-
read_group
|
26
25
|
end
|
27
26
|
|
28
27
|
# Read or re-read /etc/passwd to update the internal table of home
|
@@ -55,30 +54,6 @@ class HomeFS
|
|
55
54
|
}
|
56
55
|
end
|
57
56
|
|
58
|
-
# Read or re-read /etc/group to update the internal table of
|
59
|
-
# group membership.
|
60
|
-
# @return [void]
|
61
|
-
def read_group
|
62
|
-
# Try to read from getent
|
63
|
-
group = `getent group`
|
64
|
-
if $? != 0
|
65
|
-
group = File.read('/etc/group')
|
66
|
-
end
|
67
|
-
|
68
|
-
@groups = Hash.new
|
69
|
-
|
70
|
-
group.each_line.map { |line|
|
71
|
-
next if line.strip.empty?
|
72
|
-
_name, _, gid, members = line.split(':')
|
73
|
-
members = members.strip.split(',').map {|member| @usernames[member]}
|
74
|
-
[Integer(gid), members]
|
75
|
-
}.reject { |line|
|
76
|
-
line.nil?
|
77
|
-
}.each { |gid, members|
|
78
|
-
@groups[gid] = members
|
79
|
-
}
|
80
|
-
end
|
81
|
-
|
82
57
|
# Return the path to the root of HomeFS relative to the underlying
|
83
58
|
# filesystem. If _path_ is specified, it is taken to be relative to the
|
84
59
|
# root of HomeFS.
|
@@ -99,162 +74,28 @@ class HomeFS
|
|
99
74
|
end
|
100
75
|
end
|
101
76
|
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
# It is recommended to see
|
120
|
-
# man chmod
|
121
|
-
# to get a more detailed description about file modes.
|
122
|
-
#
|
123
|
-
# @example Test if we can write to a file
|
124
|
-
# mode_mask("/example", 0222, true)
|
125
|
-
# mode_mask("/example", 0222)
|
126
|
-
# @example Test if a file is executable by anyone
|
127
|
-
# mode_mask("/binary", 0111, false)
|
128
|
-
# @example Test if a directory has the sticky bit set
|
129
|
-
# mode_mask("/tmp", 01000, false)
|
130
|
-
#
|
131
|
-
# @param [String] file the path to the file to check
|
132
|
-
# @param [Integer] mask the mask against which to check the file's mode.
|
133
|
-
# It is recommended you write this in octal (with a leading 0).
|
134
|
-
# @param [Integer] uid if not nil, only check user and group permissions
|
135
|
-
# if the user it represents is, respectively, the owner of the
|
136
|
-
# file/in the file's group.
|
137
|
-
# @return [Boolean] whether the mode of _file_ matches the given mask
|
138
|
-
def mode_mask(file, mask, uid = nil)
|
139
|
-
stat = File.stat(file)
|
140
|
-
fmode = stat.mode
|
141
|
-
if uid
|
142
|
-
fuid, fgid = stat.uid, stat.gid
|
143
|
-
# Zero out the third digit (in octal).
|
144
|
-
# We could use a constant here, but this works
|
145
|
-
# for a mask of any length
|
146
|
-
if uid != fuid
|
147
|
-
mask &= ~(mask & 0700)
|
148
|
-
end
|
149
|
-
if !@groups[fgid].include?(uid)
|
150
|
-
mask &= ~(mask & 0070)
|
77
|
+
# If HomeFS is running as root, drop_priv sets the effective user ID
|
78
|
+
# and effective group ID to _uid_ and _gid_, respectively, and then
|
79
|
+
# calls the block passed to the method. Before returning, drop_priv
|
80
|
+
# that the EUID and EGID are each set back to 0.
|
81
|
+
# @param [Integer] uid the user ID to set the effective user ID to
|
82
|
+
# @param [Integer] gid the group ID to set the effective group ID to
|
83
|
+
# @yield an environment with the EUID and EGID set to _uid_ and _gid_
|
84
|
+
# @return the result of the block
|
85
|
+
def drop_priv(uid, gid)
|
86
|
+
if Process::Sys.getuid == 0
|
87
|
+
begin
|
88
|
+
Process::Sys.setegid(gid)
|
89
|
+
Process::Sys.seteuid(uid)
|
90
|
+
ret = yield
|
91
|
+
ensure
|
92
|
+
Process::Sys.seteuid(0)
|
93
|
+
Process::Sys.setegid(0)
|
151
94
|
end
|
152
|
-
end
|
153
|
-
fmode & mask != 0
|
154
|
-
end
|
155
|
-
|
156
|
-
# Test whether we can write to the given path. If no file exists at _path_,
|
157
|
-
# we test if the parent directory is either writable or has the sticky bit
|
158
|
-
# set.
|
159
|
-
# @param [String] path the path to test
|
160
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
161
|
-
# method returns whether the file is writable by anyone.
|
162
|
-
# @return [Boolean] whether the given path is writable
|
163
|
-
def writable?(path, uid = nil)
|
164
|
-
if !File.exist?(path)
|
165
|
-
mode_mask(File.dirname(path), 01222, uid)
|
166
95
|
else
|
167
|
-
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
# Test whether the user described by _uid_ can read from the given
|
172
|
-
# path. If no file exists at the given path, an Errno::ENOENT is
|
173
|
-
# raised.
|
174
|
-
# @param [String] path the path to test
|
175
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
176
|
-
# method returns whether the file is readable by anyone.
|
177
|
-
# @return [Boolean] whether the given path is readable
|
178
|
-
# @raise [Errno::ENOENT] if no file exists at _path_
|
179
|
-
def readable?(path, uid = nil)
|
180
|
-
mode_mask(path, 0444, uid)
|
181
|
-
end
|
182
|
-
|
183
|
-
# Test whether the user described by _uid_ can execute the given
|
184
|
-
# path. If no file exists at the given path, an Errno::ENOENT is
|
185
|
-
# raised.
|
186
|
-
# @param [String] path the path to test
|
187
|
-
# @param [Integer] uid the UID to check the privilage of. If nil, this
|
188
|
-
# method returns whether the file is executable by anyone.
|
189
|
-
# @return [Boolean] whether the given path is executable
|
190
|
-
# @raise [Errno::ENOENT] if no file exists at _path_
|
191
|
-
def executable?(path, uid = nil)
|
192
|
-
mode_mask(path, 0111, uid)
|
193
|
-
end
|
194
|
-
|
195
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
196
|
-
# write to the given path. The check is the same as in {#writable?}.
|
197
|
-
# @param [String] relpath the path to test, relative to the root of
|
198
|
-
# HomeFS (not the actual filesystem)
|
199
|
-
# @param [Integer] uid the UID to check the privilage of
|
200
|
-
# @return [void]
|
201
|
-
# @raise [Errno::EACCES] if the path is not writable by the given user
|
202
|
-
def check_writable(uid, relpath)
|
203
|
-
hpath = homepath(uid, relpath)
|
204
|
-
unless writable?(hpath, uid)
|
205
|
-
fail Errno::EACCES, relpath
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
210
|
-
# read from the given path.
|
211
|
-
# @param [String] relpath the path to test, relative to the root of
|
212
|
-
# HomeFS (not the actual filesystem)
|
213
|
-
# @param [Integer] uid the UID to check the privilage of
|
214
|
-
# @return [void]
|
215
|
-
# @raise [Errno::EACCES] if the path is not readable by the given user
|
216
|
-
# @raise [Errno::ENOENT] if no file or directory exists at the given
|
217
|
-
# path
|
218
|
-
def check_readable(uid, relpath)
|
219
|
-
hpath = homepath(uid, relpath)
|
220
|
-
unless readable?(hpath, uid)
|
221
|
-
fail Errno::EACCES, relpath
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
|
-
# This method raises an Errno::EACCES if the user given by _uid_ cannot
|
226
|
-
# list the given directory, i.e., if the user does not have the execute
|
227
|
-
# and read permissions on the given directory. If the path given is not
|
228
|
-
# a directory, the dirname of the given path is tested.
|
229
|
-
# @param [String] relpath the path to test, relative to the root of
|
230
|
-
# HomeFS (not the actual filesystem)
|
231
|
-
# @param [Integer] uid the UID to check the privilage of
|
232
|
-
# @return [void]
|
233
|
-
# @raise [Errno::EACCES] if the path is not readable by the given user
|
234
|
-
def check_listable(uid, relpath)
|
235
|
-
hpath = homepath(uid, relpath)
|
236
|
-
unless File.directory?(hpath)
|
237
|
-
hpath = File.dirname(hpath)
|
238
|
-
end
|
239
|
-
unless readable?(hpath) && executable?(hpath)
|
240
|
-
fail Errno::EACCES, relpath
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
# This method raises an Errno::EACCES if the user given by _uid_ is not
|
245
|
-
# the owner of the file described by _relpath_
|
246
|
-
# @param [String] relpath the path to test, relative to the root of
|
247
|
-
# HomeFS (not the actual filesystem)
|
248
|
-
# @param [Integer] uid the UID to check ownership of
|
249
|
-
# @return [void]
|
250
|
-
# @raise [Errno::EACCES] if the path is not owned by the given user
|
251
|
-
# @raise [Errno::ENOENT] if no file or directory exists at the given
|
252
|
-
# path
|
253
|
-
def check_owner(uid, relpath)
|
254
|
-
hpath = File.dirname(homepath(uid, relpath))
|
255
|
-
unless File.stat(hpath).uid == uid
|
256
|
-
fail Errno::EACCES, relpath
|
96
|
+
ret = yield
|
257
97
|
end
|
98
|
+
return ret
|
258
99
|
end
|
259
100
|
|
260
101
|
# Check access permissions
|
@@ -269,8 +110,16 @@ class HomeFS
|
|
269
110
|
# called directly
|
270
111
|
def access(context, path, mode)
|
271
112
|
uid = context.uid
|
272
|
-
|
273
|
-
|
113
|
+
hpath = homepath(uid, path)
|
114
|
+
drop_priv(uid, context.gid) do
|
115
|
+
access = true
|
116
|
+
check_read = (mode & 4 != 0)
|
117
|
+
check_write = (mode & 2 != 0)
|
118
|
+
check_execute = (mode & 1 != 0)
|
119
|
+
access &&= File.readable?(hpath) if check_read
|
120
|
+
access &&= File.writable?(hpath) if check_write
|
121
|
+
access &&= File.executable?(hpath) if check_execute
|
122
|
+
access
|
274
123
|
end
|
275
124
|
end
|
276
125
|
|
@@ -285,9 +134,11 @@ class HomeFS
|
|
285
134
|
# @note This method should usually only be called by FUSE, and not
|
286
135
|
# called directly
|
287
136
|
def chmod(context, path, mode)
|
288
|
-
|
289
|
-
|
290
|
-
|
137
|
+
uid = context.uid
|
138
|
+
hpath = homepath(uid, path)
|
139
|
+
drop_priv(uid, context.gid) do
|
140
|
+
File.chmod(mode, hpath)
|
141
|
+
end
|
291
142
|
end
|
292
143
|
|
293
144
|
# Change file ownership
|
@@ -302,9 +153,11 @@ class HomeFS
|
|
302
153
|
# @note This method should usually only be called by FUSE, and not
|
303
154
|
# called directly
|
304
155
|
def chown(context, path, uid, gid)
|
305
|
-
|
306
|
-
hpath = homepath(
|
307
|
-
|
156
|
+
uid = context.uid
|
157
|
+
hpath = homepath(uid, path)
|
158
|
+
drop_priv(uid, context.gid) do
|
159
|
+
File.chown(mode, hpath)
|
160
|
+
end
|
308
161
|
end
|
309
162
|
|
310
163
|
# Create and open a file
|
@@ -321,23 +174,11 @@ class HomeFS
|
|
321
174
|
# @note This method should usually only be called by FUSE, and not
|
322
175
|
# called directly
|
323
176
|
def create(context, path, mode, ffi)
|
324
|
-
|
325
|
-
hpath = homepath(
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
# race-condition. To be more specific, if this filesystem is
|
330
|
-
# running as root (which it usually is), the file will be created
|
331
|
-
# owned by root. If the mode were set when the file is created,
|
332
|
-
# then an attacker could create a setuid file writable by anyone,
|
333
|
-
# and then, in the 'real' filesystem (not HomeFS), quickly write a
|
334
|
-
# small program that just executes /bin/bash. If the write
|
335
|
-
# completed before handle.chown was called, the attacker could have
|
336
|
-
# a setuid shell.
|
337
|
-
handle = File.new(hpath, File::CREAT | File::WRONLY, 0600)
|
338
|
-
handle.chown(context.uid, context.gid)
|
339
|
-
handle.chmod(mode)
|
340
|
-
ffi.fh = handle
|
177
|
+
uid = context.uid
|
178
|
+
hpath = homepath(uid, path)
|
179
|
+
ffi.fh = drop_priv(uid, context.gid) do
|
180
|
+
File.new(hpath, File::CREAT | File::WRONLY, mode)
|
181
|
+
end
|
341
182
|
end
|
342
183
|
|
343
184
|
# Get attributes of an open file
|
@@ -431,8 +272,11 @@ class HomeFS
|
|
431
272
|
# @note This method should usually only be called by FUSE, and not
|
432
273
|
# called directly
|
433
274
|
def getattr(context, path)
|
434
|
-
|
435
|
-
|
275
|
+
uid = context.uid
|
276
|
+
hpath = homepath(uid, path)
|
277
|
+
drop_priv(uid, context.gid) do
|
278
|
+
File.lstat(hpath)
|
279
|
+
end
|
436
280
|
end
|
437
281
|
|
438
282
|
# Called when filesystem is initialised
|
@@ -457,20 +301,22 @@ class HomeFS
|
|
457
301
|
# @note This method should usually only be called by FUSE, and not
|
458
302
|
# called directly
|
459
303
|
def link(context, from, to)
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
304
|
+
uid = context.uid
|
305
|
+
hfrom = homepath(uid, from)
|
306
|
+
drop_priv(uid, context.gid) do
|
307
|
+
File.link(hfrom, to)
|
308
|
+
end
|
464
309
|
end
|
465
310
|
|
466
311
|
# (see RFuse::Fuse#mkdir)
|
467
312
|
# @note This method should usually only be called by FUSE, and not
|
468
313
|
# called directly
|
469
314
|
def mkdir(context, path, mode)
|
470
|
-
|
471
|
-
hpath = homepath(
|
472
|
-
|
473
|
-
|
315
|
+
uid = context.uid
|
316
|
+
hpath = homepath(uid, path)
|
317
|
+
drop_priv(uid, context.gid) do
|
318
|
+
Dir.mkdir(hpath, mode)
|
319
|
+
end
|
474
320
|
end
|
475
321
|
|
476
322
|
# File open operation
|
@@ -488,24 +334,17 @@ class HomeFS
|
|
488
334
|
# @note This method should usually only be called by FUSE, and not
|
489
335
|
# called directly
|
490
336
|
def open(context, path, ffi)
|
491
|
-
|
492
|
-
|
493
|
-
elsif ffi.flags & File::WRONLY != 0
|
494
|
-
mask = 0222
|
495
|
-
else
|
496
|
-
mask = 0444
|
497
|
-
end
|
498
|
-
hpath = homepath(context.uid, path)
|
499
|
-
unless mode_mask(hpath, mask, context.uid)
|
500
|
-
fail Errno::EACCES, path
|
501
|
-
end
|
337
|
+
uid = context.uid
|
338
|
+
hpath = homepath(uid, path)
|
502
339
|
|
503
340
|
# We pass the flags straight into File.open because the constants
|
504
341
|
# in RFuse::Fcntl and File::Constants have the same values, because
|
505
342
|
# they both have the same values as the open syscall. If this were
|
506
343
|
# not the case, we'd have the map the values of RFuse::Fcntl to
|
507
344
|
# their equivalents in File::Constants
|
508
|
-
|
345
|
+
drop_priv(uid, context.gid) do
|
346
|
+
ffi.fh = File.open(hpath, ffi.flags)
|
347
|
+
end
|
509
348
|
end
|
510
349
|
|
511
350
|
# Open directory
|
@@ -524,8 +363,11 @@ class HomeFS
|
|
524
363
|
# @note This method should usually only be called by FUSE, and not
|
525
364
|
# called directly
|
526
365
|
def opendir(context, path, ffi)
|
527
|
-
|
528
|
-
|
366
|
+
uid = context.uid
|
367
|
+
hpath = homepath(uid, path)
|
368
|
+
ffi.fh = drop_priv(uid, context.gid) do
|
369
|
+
Dir.new(hpath)
|
370
|
+
end
|
529
371
|
end
|
530
372
|
|
531
373
|
# Read data from an open file
|
@@ -547,7 +389,7 @@ class HomeFS
|
|
547
389
|
else
|
548
390
|
ffi.fh.seek(offset, :SET)
|
549
391
|
end
|
550
|
-
ffi.fh.read(size)
|
392
|
+
ffi.fh.read(size) || ''
|
551
393
|
end
|
552
394
|
|
553
395
|
# (see RFuse::Fuse#readdir)
|
@@ -603,12 +445,12 @@ class HomeFS
|
|
603
445
|
# @note This method should usually only be called by FUSE, and not
|
604
446
|
# called directly
|
605
447
|
def rename(context, from, to)
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
448
|
+
uid = context.uid
|
449
|
+
hfrom = homepath(uid, from)
|
450
|
+
hto = homepath(uid, to)
|
451
|
+
drop_priv(uid, context.gid) do
|
452
|
+
FileUtils.mv(hfrom, hto, :force => true)
|
453
|
+
end
|
612
454
|
end
|
613
455
|
|
614
456
|
# Create a symbolic link
|
@@ -624,12 +466,10 @@ class HomeFS
|
|
624
466
|
# @note This method should usually only be called by FUSE, and not
|
625
467
|
# called directly
|
626
468
|
def symlink(context, to, from)
|
627
|
-
|
628
|
-
hfrom = homepath(
|
629
|
-
|
630
|
-
|
631
|
-
File.lchown(context.uid, context.gid, hfrom)
|
632
|
-
rescue
|
469
|
+
uid = context.uid
|
470
|
+
hfrom = homepath(uid, from)
|
471
|
+
drop_priv(uid, context.gid) do
|
472
|
+
File.symlink(to, hfrom)
|
633
473
|
end
|
634
474
|
end
|
635
475
|
|
@@ -644,8 +484,11 @@ class HomeFS
|
|
644
484
|
# @note This method should usually only be called by FUSE, and not
|
645
485
|
# called directly
|
646
486
|
def truncate(context, path, offset)
|
647
|
-
|
648
|
-
|
487
|
+
uid = context.uid
|
488
|
+
hpath = homepath(uid, path)
|
489
|
+
drop_priv(uid, context.gid) do
|
490
|
+
File.truncate(hpath, offset)
|
491
|
+
end
|
649
492
|
end
|
650
493
|
|
651
494
|
# Remove a file
|
@@ -658,8 +501,11 @@ class HomeFS
|
|
658
501
|
# @note This method should usually only be called by FUSE, and not
|
659
502
|
# called directly
|
660
503
|
def unlink(context, path)
|
661
|
-
|
662
|
-
|
504
|
+
uid = context.uid
|
505
|
+
hpath = homepath(uid, path)
|
506
|
+
drop_priv(uid, context.gid) do
|
507
|
+
File.unlink(hpath)
|
508
|
+
end
|
663
509
|
end
|
664
510
|
|
665
511
|
# Change access/modification times of a file
|
@@ -674,8 +520,11 @@ class HomeFS
|
|
674
520
|
# @note This method should usually only be called by FUSE, and not
|
675
521
|
# called directly
|
676
522
|
def utimens(context, path, actime, modtime)
|
677
|
-
|
678
|
-
|
523
|
+
uid = context.uid
|
524
|
+
hpath = homepath(uid, path)
|
525
|
+
drop_priv(uid, context.gid) do
|
526
|
+
File.utime(actime, modtime, hpath)
|
527
|
+
end
|
679
528
|
end
|
680
529
|
|
681
530
|
# Write data to an open file
|
@@ -717,9 +566,11 @@ begin
|
|
717
566
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
718
567
|
# available
|
719
568
|
def getxattr(context, path, name)
|
720
|
-
|
721
|
-
hpath = homepath(
|
722
|
-
|
569
|
+
uid = context.uid
|
570
|
+
hpath = homepath(uid, path)
|
571
|
+
drop_priv(uid, context.gid) do
|
572
|
+
check_nodata Xattr::Lib.get(hpath, false, name)
|
573
|
+
end
|
723
574
|
end
|
724
575
|
|
725
576
|
# Set extended attributes
|
@@ -737,9 +588,11 @@ begin
|
|
737
588
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
738
589
|
# available
|
739
590
|
def setxattr(context, path, name, data, flags)
|
740
|
-
|
741
|
-
hpath = homepath(
|
742
|
-
|
591
|
+
uid = context.uid
|
592
|
+
hpath = homepath(uid, path)
|
593
|
+
drop_priv(uid, context.gid) do
|
594
|
+
Xattr::Lib.set(hpath, false, name, data)
|
595
|
+
end
|
743
596
|
end
|
744
597
|
|
745
598
|
# List extended attributes
|
@@ -754,9 +607,11 @@ begin
|
|
754
607
|
# @note This method is only defined if the gem 'ffi-xattr' is
|
755
608
|
# available
|
756
609
|
def listxattr(context, path)
|
757
|
-
|
758
|
-
hpath = homepath(
|
759
|
-
|
610
|
+
uid = context.uid
|
611
|
+
hpath = homepath(uid, path)
|
612
|
+
drop_priv(uid, context.gid) do
|
613
|
+
check_nodata Xattr::Lib.list(hpath, false)
|
614
|
+
end
|
760
615
|
end
|
761
616
|
|
762
617
|
# Remove extended attribute
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: homefs
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dylan Frese
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-10-
|
11
|
+
date: 2015-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rfuse
|