ndr_support 3.1.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.
Files changed (67) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +14 -0
  3. data/.rubocop.yml +27 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +22 -0
  6. data/CODE_OF_CONDUCT.md +13 -0
  7. data/Gemfile +4 -0
  8. data/Guardfile +16 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +91 -0
  11. data/Rakefile +12 -0
  12. data/code_safety.yml +258 -0
  13. data/gemfiles/Gemfile.rails32 +6 -0
  14. data/gemfiles/Gemfile.rails32.lock +108 -0
  15. data/gemfiles/Gemfile.rails41 +6 -0
  16. data/gemfiles/Gemfile.rails41.lock +111 -0
  17. data/gemfiles/Gemfile.rails42 +6 -0
  18. data/gemfiles/Gemfile.rails42.lock +111 -0
  19. data/lib/ndr_support.rb +21 -0
  20. data/lib/ndr_support/array.rb +52 -0
  21. data/lib/ndr_support/concerns/working_days.rb +94 -0
  22. data/lib/ndr_support/date_and_time_extensions.rb +103 -0
  23. data/lib/ndr_support/daterange.rb +196 -0
  24. data/lib/ndr_support/fixnum/calculations.rb +15 -0
  25. data/lib/ndr_support/fixnum/julian_date_conversions.rb +14 -0
  26. data/lib/ndr_support/hash.rb +52 -0
  27. data/lib/ndr_support/integer.rb +12 -0
  28. data/lib/ndr_support/nil.rb +38 -0
  29. data/lib/ndr_support/ourdate.rb +97 -0
  30. data/lib/ndr_support/ourtime.rb +51 -0
  31. data/lib/ndr_support/regexp_range.rb +65 -0
  32. data/lib/ndr_support/safe_file.rb +185 -0
  33. data/lib/ndr_support/safe_path.rb +268 -0
  34. data/lib/ndr_support/string/cleaning.rb +136 -0
  35. data/lib/ndr_support/string/conversions.rb +137 -0
  36. data/lib/ndr_support/tasks.rb +1 -0
  37. data/lib/ndr_support/time/conversions.rb +13 -0
  38. data/lib/ndr_support/utf8_encoding.rb +72 -0
  39. data/lib/ndr_support/utf8_encoding/control_characters.rb +53 -0
  40. data/lib/ndr_support/utf8_encoding/force_binary.rb +44 -0
  41. data/lib/ndr_support/utf8_encoding/object_support.rb +31 -0
  42. data/lib/ndr_support/version.rb +5 -0
  43. data/lib/ndr_support/yaml/serialization_migration.rb +65 -0
  44. data/lib/tasks/audit_code.rake +423 -0
  45. data/ndr_support.gemspec +39 -0
  46. data/test/array_test.rb +20 -0
  47. data/test/concerns/working_days_test.rb +122 -0
  48. data/test/daterange_test.rb +194 -0
  49. data/test/fixnum/calculations_test.rb +28 -0
  50. data/test/hash_test.rb +84 -0
  51. data/test/integer_test.rb +14 -0
  52. data/test/nil_test.rb +40 -0
  53. data/test/ourdate_test.rb +27 -0
  54. data/test/ourtime_test.rb +27 -0
  55. data/test/regexp_range_test.rb +135 -0
  56. data/test/resources/filesystem_paths.yml +37 -0
  57. data/test/safe_file_test.rb +597 -0
  58. data/test/safe_path_test.rb +168 -0
  59. data/test/string/cleaning_test.rb +176 -0
  60. data/test/string/conversions_test.rb +353 -0
  61. data/test/test_helper.rb +41 -0
  62. data/test/time/conversions_test.rb +15 -0
  63. data/test/utf8_encoding/control_characters_test.rb +84 -0
  64. data/test/utf8_encoding/force_binary_test.rb +64 -0
  65. data/test/utf8_encoding_test.rb +170 -0
  66. data/test/yaml/serialization_test.rb +145 -0
  67. metadata +295 -0
@@ -0,0 +1,51 @@
1
+ require 'active_support/time'
2
+ require 'ndr_support/ourdate'
3
+
4
+ # Convert a string into a time value (timestamp)
5
+ # (helped by String.thetime)
6
+ class Ourtime
7
+ attr_reader :thetime
8
+
9
+ def initialize(x = nil)
10
+ if x.is_a?(Time)
11
+ @thetime = x
12
+ elsif x.is_a?(Date)
13
+ @thetime = x.to_time
14
+ elsif x.is_a?(String)
15
+ self.source = x
16
+ else
17
+ @thetime = nil
18
+ end
19
+ end
20
+
21
+ def to_s
22
+ @thetime ? @thetime.to_time.to_s(:ui) : ''
23
+ end
24
+
25
+ def empty?
26
+ # An unspecified time will be empty. A valid or invalid time will not.
27
+ @thetime.nil? && @source.blank?
28
+ end
29
+
30
+ def source=(s)
31
+ begin
32
+ # Re-parse our own timestamps [+- seconds] without swapping month / day
33
+ @thetime = DateTime.strptime(s, '%d.%m.%Y %H:%M:%S').to_time
34
+ rescue ArgumentError
35
+ begin
36
+ @thetime = DateTime.strptime(s, '%d.%m.%Y %H:%M').to_time
37
+ rescue ArgumentError
38
+ @thetime = Time.parse(s)
39
+ end
40
+ end
41
+ # Apply timezone correction for daylight saving
42
+ if @thetime
43
+ @thetime = Ourdate.build_datetime(@thetime.year, @thetime.month,
44
+ @thetime.day, @thetime.hour,
45
+ @thetime.min, @thetime.sec,
46
+ @thetime.instance_of?(Time) ? @thetime.usec : 0).to_time
47
+ end
48
+ end
49
+
50
+ private :source=
51
+ end
@@ -0,0 +1,65 @@
1
+ # This class provides the ability to define a range using numbers or regular expressions
2
+ # and when provided with an array, will return a normal ruby Range object based on the matching
3
+ # elements in the array. NOTE this class is has the same attributes as Range and is identical
4
+ # when serialized (except for the class declaration obviously), but it is NOT a substitute for
5
+ # Range (only a facade).
6
+ class RegexpRange
7
+ class PatternMatchError < StandardError
8
+ end
9
+
10
+ attr_reader :begin, :end, :excl
11
+
12
+ def initialize(range_start, range_end, exclusive = false)
13
+ @begin = range_start
14
+ @end = range_end
15
+ @excl = exclusive
16
+ end
17
+
18
+ def to_range(lines)
19
+ start_line_number = @begin
20
+ if start_line_number.is_a?(Regexp)
21
+ lines.each_with_index do |line, i|
22
+ if line.match(start_line_number)
23
+ start_line_number = i
24
+ break
25
+ end
26
+ end
27
+
28
+ if start_line_number.is_a?(Regexp)
29
+ fail PatternMatchError, "begin pattern #{start_line_number.inspect} not found"
30
+ end
31
+ end
32
+
33
+ end_line_number = @end
34
+ if end_line_number.is_a?(Regexp)
35
+ start_scan_line = start_line_number + 1
36
+ lines[start_scan_line..-1].each_with_index do |line, i|
37
+ # puts "##{start_scan_line + i}: #{line}"
38
+ if line.match(end_line_number)
39
+ end_line_number = start_scan_line + i
40
+ break
41
+ end
42
+ end
43
+ if end_line_number.is_a?(Regexp)
44
+ fail PatternMatchError,
45
+ "end pattern #{end_line_number.inspect} not found on or after line #{start_scan_line}"
46
+ end
47
+ end
48
+
49
+ Range.new(start_line_number, end_line_number, @excl)
50
+ end
51
+
52
+ # `other` is equal to self if it is a RegexpRange with the same state.
53
+ def ==(other)
54
+ other.is_a?(RegexpRange) && other.state == state
55
+ end
56
+ alias_method :eql?, :==
57
+
58
+ protected
59
+
60
+ # Used by other RegexpRange objects, as well as Hashes, during equality checks:
61
+ def state
62
+ [@begin, @end, @excl]
63
+ end
64
+ delegate :hash, :to => :state # Used for Hash key lookup
65
+ end
@@ -0,0 +1,185 @@
1
+ class SafeFile
2
+ def initialize(*args)
3
+ a = self.class.get_fname_mode_prms(*args)
4
+ fname = a[0]
5
+ mode = a[1]
6
+ prms = a[2]
7
+
8
+ if prms
9
+ @file = File.new(fname, mode, prms)
10
+ else
11
+ @file = File.new(fname, mode)
12
+ end
13
+
14
+ # Just in case better clone the object
15
+ # Ruby object are passed by reference
16
+ @file_name = fname.clone
17
+ end
18
+
19
+ def self.open(*args)
20
+ return SafeFile.new(*args) unless block_given?
21
+
22
+ f = SafeFile.new(*args)
23
+ yield f
24
+ f.close
25
+ end
26
+
27
+ def close
28
+ @file.close
29
+ end
30
+
31
+ def read
32
+ verify @file_name, 'r'
33
+ @file.read
34
+ end
35
+
36
+ def write(data)
37
+ verify @file_name, 'w'
38
+ @file.write(data)
39
+ end
40
+
41
+ def path
42
+ @file_name.clone
43
+ end
44
+
45
+ def self.extname(file_name)
46
+ verify file_name
47
+ File.extname(file_name)
48
+ end
49
+
50
+ def self.read(file_name)
51
+ verify file_name, 'r'
52
+ File.read(file_name)
53
+ end
54
+
55
+ def self.readlines(*args)
56
+ fail ArgumentError, "Incorrect number of arguments - #{args.length}" if args.length > 2 or args.length == 0
57
+ verify args[0], 'r'
58
+ File.readlines(*args)
59
+ end
60
+
61
+ def self.directory?(file_name)
62
+ verify file_name
63
+ File.directory?(file_name)
64
+ end
65
+
66
+ def self.exist?(file_name)
67
+ self.exists?(file_name)
68
+ end
69
+
70
+ def self.exists?(file_name)
71
+ verify file_name
72
+ File.exist?(file_name)
73
+ end
74
+
75
+ def self.file?(file_name)
76
+ verify file_name
77
+ File.file?(file_name)
78
+ end
79
+
80
+ def self.zero?(file_name)
81
+ verify file_name
82
+ File.zero?(file_name)
83
+ end
84
+
85
+ def self.basename(file_name, suffix = :none)
86
+ verify file_name
87
+ if suffix == :none
88
+ File.basename(file_name)
89
+ else
90
+ File.basename(file_name, suffix)
91
+ end
92
+ end
93
+
94
+ def self.safepath_to_string(fname)
95
+ verify fname
96
+ fname.to_s
97
+ end
98
+
99
+ def self.basename_file
100
+ # SECURE: 02-08-2012 TPG Can't assign to __FILE__
101
+ File.basename(__FILE__)
102
+ end
103
+
104
+ def self.dirname(path)
105
+ verify path
106
+ res = path.clone
107
+ res.path = File.dirname(path)
108
+ res
109
+ end
110
+
111
+ def self.delete(*list)
112
+ verify list, 'w'
113
+
114
+ list.each do |file|
115
+ File.delete(file) if File.exist?(file)
116
+ end.length
117
+ end
118
+
119
+ private
120
+
121
+ def verify(file_names, prm = nil)
122
+ self.class.verify(file_names, prm)
123
+ end
124
+
125
+ def self.verify(file_names, prm = nil)
126
+ [file_names].flatten.each do |file_name|
127
+ fail ArgumentError, "file_name should be of type SafePath, but it is #{file_name.class}" unless file_name.class == SafePath
128
+
129
+ if prm
130
+ [prm].flatten.each do |p|
131
+ fail SecurityError, "Permissions denied. Cannot access the file #{file_name} with permissions #{prm}. The permissions are #{file_name.permissions}" unless file_name.permissions.include?(p)
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def self.verify_mode(file_name, mode)
138
+ if mode.match(/\A(r\+)|(w\+)|(a\+)\Z/)
139
+ verify file_name, ['w', 'r']
140
+ elsif mode.match(/\Aw|a\Z/)
141
+ verify file_name, ['w']
142
+ elsif mode.match(/\Ar\Z/)
143
+ verify file_name, ['r']
144
+ else
145
+ fail ArgumentError, "Incorrect mode. It should be one of: 'r', 'w', 'r+', 'w+', 'a', 'a+'"
146
+ end
147
+ end
148
+
149
+ def self.get_fname_mode_prms(*args)
150
+ case args.length
151
+ when 1
152
+ verify_mode(args[0], 'r')
153
+ fname = args[0]
154
+ mode = 'r'
155
+ prms = nil
156
+
157
+ when 2
158
+ fail ArgimentError if args[1].class != Fixnum and args[1].class != String
159
+
160
+ if args[1].class == Fixnum
161
+ verify_mode(args[0], 'r')
162
+ mode = 'r'
163
+ prms = args[1]
164
+ else
165
+ verify_mode(args[0], args[1])
166
+ mode = args[1]
167
+ prms = nil
168
+ end
169
+
170
+ fname = args[0]
171
+
172
+ when 3
173
+ fail ArgimentError if args[1].class != String or args[2].class != Fixnum
174
+ verify_mode(args[0], args[1])
175
+
176
+ fname = args[0]
177
+ mode = args[1]
178
+ prms = args[2]
179
+ else
180
+ fail ArgumentError, "Incorrect number of arguments #{args.length}"
181
+ end
182
+
183
+ [fname, mode, prms]
184
+ end
185
+ end
@@ -0,0 +1,268 @@
1
+ require 'erb'
2
+
3
+ # Ruby has a built-in SecurityError class
4
+ #class SecurityError < StandardError
5
+ #end
6
+
7
+ # = SafePath
8
+ #
9
+ # SafePath is a class which contains path to a file or directory. It also holds "path space" and
10
+ # permissions. The path space is a directory. Everything in this directory and all the
11
+ # subdirectories can be accessed with the permissions given to the constructor. The instance of
12
+ # the class checks whether the path constructed points to a directory, whcih is in the
13
+ # "path space". The idea is to limit the access of the program to given directory.
14
+ #
15
+ # Example of usage is :
16
+ # sp = SafePath("dbs_inbox")
17
+ #
18
+ # The root directory of the pathspace is in the file config/filesystem_paths.yml . In this case dbs_inbox has root
19
+ # /mounts/ron/dbs_inbox . Every path which starts with /mount/ron/dbs_inbox is considered as safe. If a path is constructed
20
+ # which is /mounts/ron/dbs_inbox/../../../etc/passwd for example then the class will evaluate the path and it will
21
+ # raise exception SecurityError.
22
+ #
23
+ # The paths can be constructed by using +, join or join!.
24
+ # Example:
25
+ # This:
26
+ # sp = SafePath("dbs_inbox")
27
+ # sp + "/my_dir"
28
+ # Points to:
29
+ # /mounts/ron/dbs_inbox/my_dir
30
+ #
31
+ #
32
+ # The functions join and join! work in similar way. The difference between
33
+ # join and join! is that join creates new instance of the class SafePath and
34
+ # return it and join! doesn't create new instance, but works in-place and after
35
+ # that it returns reference to the current instance.
36
+ # The both operators can be used like that:
37
+ # sp.join("/my_dir") #this is the same as sp + "my_dir"
38
+ # sp.join!("/my_dir") #this is NOT the same as sp "my_dir"
39
+ #
40
+ # Warning the function sp.path = "some_path" will treat some_path as absolute path
41
+ # and if it doesn't point to the root it will raise exception. The danger is that
42
+ # it returns the argument on the right hand side. So if it is a string the operator
43
+ # will return a string. This is the way ruby works. If it is used properly it shouldn't
44
+ # be a problem. The best way to use it is:
45
+ # sp.path = sp.root + "my_dir"
46
+ # sp.root returns SafePath and after that + is called which also returns SafePath. So the
47
+ # right hand side of the expression is SafePath and the = will return SafePath.
48
+ #
49
+ class SafePath
50
+
51
+ # Returns the list of safe 'root' filesystem locations, or raises
52
+ # a SecurityError if no configuration has been provided.
53
+ def self.fs_paths
54
+ if defined?(@@fs_paths)
55
+ @@fs_paths
56
+ else
57
+ fail SecurityError, 'SafePath not configured!'
58
+ end
59
+ end
60
+
61
+ # Takes the path the filesystem_paths.yml file that
62
+ # should be used. Attempting to reconfigure with
63
+ # new settings will raise a security error.
64
+ def self.configure!(filepath)
65
+ if defined?(@@fs_paths)
66
+ fail SecurityError, 'Attempt to re-assign SafePath config!'
67
+ else
68
+ File.open(filepath, 'r') do |file|
69
+ @@fs_paths = YAML.load(ERB.new(file.read).result)
70
+ @@fs_paths.freeze
71
+ end
72
+ end
73
+ end
74
+
75
+ # Takes:
76
+ # * path - This is a path to a directory. Usually a string.
77
+ # * path_space - This is identifier of the path space in whichi the system should work.
78
+ # it is a string. To find list of path spaces with their roots, please
79
+ # see config/filesystem_paths.yml
80
+ #
81
+ # Raises:
82
+ # * ArgumentError
83
+ # * SecurityError
84
+ #
85
+ def initialize(path_space, root_suffix = '')
86
+ # The class has to use different path definitions during test
87
+
88
+ fs_paths = self.class.fs_paths
89
+
90
+ platform = fs_paths.keys.select do |key|
91
+ RUBY_PLATFORM.match key
92
+ end[0]
93
+
94
+ root_path_regexp = if platform == /mswin32|mingw32/
95
+ /\A([A-Z]:[\\\/]|\/\/)/
96
+ else
97
+ /\A\//
98
+ end
99
+
100
+ fail ArgumentError, "The space #{path_space} doesn't exist. Please choose one of: #{fs_paths[platform].keys.inspect}" unless fs_paths[platform][path_space]
101
+ fail ArgumentError, "The space #{path_space} is broken. The root path should be absolute path but it was #{fs_paths[platform][path_space]['root']}" unless fs_paths[platform][path_space]['root'].match(root_path_regexp)
102
+ fail ArgumentError, "The space #{path_space} is broken. No permissions specified}" unless fs_paths[platform][path_space]['prms']
103
+
104
+ # The function verify uses @root. Therefore first assign the root path
105
+ # specified in the yaml file. After that verify that the root path
106
+ # specified from the contructor is subpath of the path specified in the
107
+ # yaml file. If it is not raise exception before anything else is done.
108
+ #
109
+ # The reason to assign @root 2 times is that it is better to have the
110
+ # logic verifing the path in only one function. This way it is going
111
+ # to be easier to maintain it and keep it secure.
112
+ #
113
+
114
+ @root = File.expand_path fs_paths[platform][path_space]['root']
115
+ @root = verify(File.join(fs_paths[platform][path_space]['root'], root_suffix)) unless root_suffix.blank?
116
+
117
+ @path_space = path_space
118
+ @maximum_prms = fs_paths[platform][path_space]['prms']
119
+
120
+ self.path = @root
121
+ end
122
+
123
+ def ==(other)
124
+ other.class == SafePath and other.root.to_s == @root and other.permissions == self.permissions and other.to_s == self.to_s
125
+ end
126
+
127
+ # WARNING: do not use sp.to_s + from_attacker . This is unsafe!
128
+ #
129
+ # Returns:
130
+ # String
131
+ #
132
+ def to_s
133
+ @path
134
+ end
135
+
136
+ # WARNING: do not use s.to_s + "my_path" . This is unsafe!
137
+ #
138
+ # Returns:
139
+ # String
140
+ #
141
+ def to_str
142
+ self.to_s
143
+ end
144
+
145
+ # Getter for permissions.
146
+ #
147
+ # Returns:
148
+ # Array
149
+ def permissions
150
+ if @prm
151
+ @prm
152
+ else
153
+ @maximum_prms
154
+ end
155
+ end
156
+
157
+ # Getter for path space identifier.
158
+ #
159
+ # Returns:
160
+ # Array
161
+ #
162
+ def path_space
163
+ @path_space
164
+ end
165
+
166
+ # Getter for the root of the path space.
167
+ #
168
+ # Returns:
169
+ # SafePath
170
+ #
171
+ def root
172
+ r = self.clone
173
+ r.path = @root
174
+ r.permissions = self.permissions
175
+ r
176
+ end
177
+
178
+ # The permissions are specified in the yml file, but this function
179
+ # can select subset of these permissions. It cannot select permission,
180
+ # which is not specified in the yml file.
181
+ #
182
+ # Takes:
183
+ # * Array of permission or single permission - If it is array then
184
+ # tha array could contain duplicates and it also can be nested.
185
+ # All the duplicates will be removed and the array will be flattened
186
+ #
187
+ # Returns:
188
+ # Array - this is the reuslt of the assignment. Note it is the right hand side of the expression
189
+ #
190
+ # Raises:
191
+ # * ArgumentError
192
+ #
193
+ def permissions=(permissions)
194
+ err_mess = "permissions has to be one or more of the values: #{@maximum_prms.inspect}\n but it was #{permissions.inspect}"
195
+
196
+ @prm = [permissions].flatten.each do |prm|
197
+ fail ArgumentError, err_mess unless @maximum_prms.include?(prm)
198
+ end.uniq
199
+ end
200
+
201
+ # Setter for path.
202
+ #
203
+ # Takes:
204
+ # * path - The path.
205
+ #
206
+ # Returns:
207
+ # Array - this is the result of the assignment. Note it is the right hand side of the expression
208
+ #
209
+ # Raises:
210
+ # * SecurityError
211
+ #
212
+ # Warning avoid using this in expressions like this (safe_path_new = (safe_path.path = safe_path.root.to_s + "/test")) + path_from_attacker
213
+ # This is unsafe!
214
+ #
215
+ def path=(path)
216
+ @path = verify(path)
217
+ self
218
+ end
219
+
220
+ # Another name for join
221
+ def +(path)
222
+ self.join(path)
223
+ end
224
+
225
+ # Used to construct path. It joins the current path with the given one.
226
+ # Takes:
227
+ # * path - path to be concatenated
228
+ #
229
+ # Returns:
230
+ # New instance of SafePath which contains the new path.
231
+ #
232
+ # Raises:
233
+ # * SecurityError
234
+ #
235
+ def join(path)
236
+ r = self.clone
237
+ r.path = File.join(@path, path)
238
+ r
239
+ end
240
+
241
+ # Used to construct path. It joins the current path with the given one.
242
+ # Takes:
243
+ # * path - path to be concatenated
244
+ #
245
+ # Returns:
246
+ # Reference to the current instance. It works in-place
247
+ #
248
+ # Raises:
249
+ # * SecurityError
250
+ #
251
+ def join!(path)
252
+ self.path = File.join(@path, path)
253
+ self
254
+ end
255
+
256
+ def length()
257
+ @path.length
258
+ end
259
+
260
+ private
261
+
262
+ # Verifies whether the path is safe.
263
+ def verify(path)
264
+ epath = File.expand_path(path)
265
+ fail SecurityError, "The given path is insecure. The path should point at #{@root}, but it points at #{epath}" unless epath.match(/\A#{Regexp.quote(@root)}/)
266
+ epath
267
+ end
268
+ end