class Phlex::CSV
Constants
- FORMULA_PREFIXES_MAP
- UNDEFINED
Attributes
Public Class Methods
Source
# File lib/phlex/csv.rb, line 12 def initialize(collection) @collection = collection @_row_buffer = [] @_headers = [] @_row_appender = nil end
Public Instance Methods
Source
# File lib/phlex/csv.rb, line 125 def around_row(...) row_template(...) @row_appender.call end
Source
# File lib/phlex/csv.rb, line 21 def call(buffer = +"", context: nil, delimiter: self.delimiter) ensure_escape_csv_injection_configured! strip_whitespace = trim_whitespace? escape_csv_injection = escape_csv_injection? row_buffer = @_row_buffer headers = @_headers has_yielder = respond_to?(:yielder, true) first_row = true render_headers = render_headers? if delimiter.length != 1 raise Phlex::ArgumentError.new("Delimiter must be a single character") end if strip_whitespace escape_regex = /[\n"#{delimiter}]/ else escape_regex = /^\s|\s$|[\n"#{delimiter}]/ end if has_yielder warn <<~MESSAGE Custom yielders are deprecated in Phlex::CSV. Please replace your yielder with an `around_row` method. You should be able to just rename your yielder method and change `yield` to `super`. MESSAGE end row_appender = -> { row = row_buffer if first_row first_row = false i = 0 number_of_columns = row.length first_col = true while i < number_of_columns header, = row[i] headers[i] = header if render_headers if first_col first_col = false else buffer << delimiter end __escape__(buffer, header, escape_csv_injection:, strip_whitespace:, escape_regex:) end i += 1 end buffer << "\n" if render_headers end i = 0 number_of_columns = row.length first_col = true while i < number_of_columns header, value = row[i] unless headers[i] == header raise Phlex::RuntimeError.new("Header mismatch at index #{i}: expected #{headers[i]}, got #{header}.") end if first_col first_col = false else buffer << delimiter end __escape__(buffer, value, escape_csv_injection:, strip_whitespace:, escape_regex:) i += 1 end buffer << "\n" row_buffer.clear } if has_yielder each_item do |record| yielder(record) do |*a, **k| row_template(*a, **k) row_appender.call end end else @row_appender = row_appender each_item do |record| around_row(record) end end buffer end
Private Instance Methods
Source
# File lib/phlex/csv.rb, line 165 def __escape__(buffer, value, escape_csv_injection:, strip_whitespace:, escape_regex:) value = case value when String value when Symbol value.name else value.to_s end if strip_whitespace value = value.strip if escape_csv_injection if value.empty? buffer << value elsif FORMULA_PREFIXES_MAP[value.getbyte(0)] value.gsub!('"', '""') buffer << '"\'' << value << '"' elsif value.match?(escape_regex) value.gsub!('"', '""') buffer << '"' << value << '"' else buffer << value end else # not escaping CSV injection buffer << value end else # not stripping whitespace if escape_csv_injection first_byte = value.getbyte(0) if value.empty? buffer << '""' elsif FORMULA_PREFIXES_MAP[first_byte] buffer << '"\'' << value.gsub('"', '""') << '"' elsif value.match?(escape_regex) buffer << '"' << value.gsub('"', '""') << '"' else buffer << value end else # not escaping CSV injection if value.empty? buffer << '""' elsif value.match?(escape_regex) buffer << '"' << value.gsub('"', '""') << '"' else buffer << value end end end end
Source
# File lib/phlex/csv.rb, line 142 def column(header = nil, value) @_row_buffer << [header, value] end
Source
# File lib/phlex/csv.rb, line 234 def ensure_escape_csv_injection_configured! if escape_csv_injection? == UNDEFINED raise <<~MESSAGE You need to define `escape_csv_injection?` in #{self.class.name}. CSV injection is a security vulnerability where malicious spreadsheet formulae are used to execute code or exfiltrate data when a CSV is opened in a spreadsheet program such as Microsoft Excel or Google Sheets. For more information, see https://owasp.org/www-community/attacks/CSV_Injection If you’re sure this CSV will never be opened in a spreadsheet program, you can *disable* CSV injection escapes: def escape_csv_injection? = false This is useful when using CSVs for byte-for-byte data exchange between secure systems. Alternatively, you can *enable* CSV injection escapes at the cost of data integrity: def escape_csv_injection? = true Enabling the CSV injection escapes will prefix with a single quote `'` any values that start with: `=`, `+`, `-`, `@`, `\\t`, `\\r` Unfortunately, there is no one-size-fits-all solution to CSV injection. You need to decide based on your specific use case. MESSAGE end end
Source
# File lib/phlex/csv.rb, line 161 def escape_csv_injection? UNDEFINED end
Override and set to ‘false` to disable CSV injection escapes or `true` to enable.
Source
# File lib/phlex/csv.rb, line 224 def method_missing(method_name, ...) if method_name == :row_template && respond_to?(:view_template) warn "Deprecated: Use `row_template` instead of `view_template` in Phlex CSVs." self.class.alias_method :row_template, :view_template view_template(...) else super end end
Handle legacy ‘view_template` method
Calls superclass method
Source
# File lib/phlex/csv.rb, line 151 def render_headers? true end
Override and set to ‘false` to disable rendering headers.
Source
# File lib/phlex/csv.rb, line 219 def respond_to_missing?(method_name, include_private) (method_name == :row_template && respond_to?(:view_template)) || super end
Handle legacy ‘view_template` method
Calls superclass method
Source
# File lib/phlex/csv.rb, line 156 def trim_whitespace? false end
Override and set to ‘true` to strip leading and trailing whitespace from values.