lib/aef/hosts/file.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# encoding: UTF-8
=begin
Copyright Alexander E. Fischer <aef@raxys.net>, 2012

This file is part of Hosts.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
=end

require 'aef/hosts'

module Aef
  module Hosts

    # This class represents a hosts file and aggregates its elements.
    #
    # It is able to parse host files from file-system or String and can
    # generate a String representation of itself to String or file-system.
    class File

      include Helpers

      # Regular expression to extract a comment line.
      #
      # @private
      COMMENT_LINE_PATTERN   = /^\s*#(.*)$/

      # Regular expression to extract section headers and footers.
      #
      # @private
      SECTION_MARKER_PATTERN = /^ -----(BEGIN|END) SECTION (.*)-----(?:[\r])?$/

      # Regular expression to extract entry lines.
      #
      # @private
      ENTRY_LINE_PATTERN     = /^([^#]*)(?:#(.*))?$/

      # The hosts file's elements.
      #
      # @return [Array<Aef::Hosts::Element>]
      attr_accessor :elements

      # The filesystem path of the hosts file.
      #
      # @return [Pathname, nil]
      attr_reader :path

      class << self
        # Parses a hosts file given as path.
        #
        # @param [Pathname] path the hosts file path
        # @return [Aef::Hosts::File] a file
        def read(path)
          new(path).read
        end

        # Parses a hosts file given as String.
        #
        # @param [String] data a String representation of the hosts file
        # @return [Aef::Hosts::File] a file
        def parse(data)
          new.parse(data)
        end
      end

      # Initializes a file.
      #
      # @param [Pathname] path path to the hosts file
      def initialize(path = nil)
        reset
        self.path = path
      end

      # Removes all elements.
      #
      # @return [Aef::Hosts::File] a self reference
      def reset
        @elements = []

        self
      end

      # Deletes the cached String representations of all elements.
      #
      # @return [Aef::Hosts::File] a self reference
      def invalidate_cache!
        elements.each do |element|
          element.invalidate_cache!
        end

        self
      end

      # Sets the filesystem path of the hosts file.
      def path=(path)
        @path = to_pathname(path)
      end

      # Parses a hosts file given as path.
      #
      # @param [Pathname] path override the path attribute for this operation
      # @return [Aef::Hosts::File] a self reference
      def read(path = nil)
        path = path.nil? ? @path : to_pathname(path)

        raise ArgumentError, 'No path given' unless path

        parse(path.read)

        self
      end

      # Parses a hosts file given as String.
      #
      # @param [String] data a String representation of the hosts file
      # @return [Aef::Hosts::File] a self reference
      def parse(data)
        current_section = self

        data.to_s.lines.each_with_index do |line, line_number|
          line = Linebreak.encode(line, :unix)

          if COMMENT_LINE_PATTERN =~ line
            comment = $1

            if SECTION_MARKER_PATTERN =~ comment
              type = $1
              name = $2

              case type
              when 'BEGIN'
                unless current_section.is_a?(Section)
                  current_section = Section.new(
                    name,
                    :cache => {:header => line, :footer => nil}
                  )
                else
                  raise ParserError, "Invalid cascading of sections. Cannot start new section '#{name}' without first closing section '#{current_section.name}' on line #{line_number + 1}."
                end
              when 'END'
                if name == current_section.name
                  current_section.cache[:footer] = line
                  elements << current_section
                  current_section = self
                else
                  raise ParserError, "Invalid closing of section. Found attempt to close section '#{name}' in body of section '#{current_section.name}' on line #{line_number + 1}."
                end
              end
            else
              current_section.elements << Comment.new(
                comment,
                :cache => line
              )
            end
          else
            ENTRY_LINE_PATTERN =~ line

            entry   = $1
            comment = $2

            if entry and not entry =~ /^\s+$/

              split = entry.split(/\s+/)
              split.shift if split.first == ''

              address, name, *aliases = *split

              current_section.elements << Entry.new(
                address, name,
                :aliases => aliases,
                :comment => comment,
                :cache   => line
              )
            else
              current_section.elements << EmptyElement.new(
                :cache => line
              )
            end
          end
        end

        self
      end

      # Generates a hosts file and writes it to a path.
      #
      # @param [Hash] options
      # @option options [Pathname] :path overrides the path attribute for this
      #   operation
      # @option options [true, false] :force_generation if set to true, the
      #   cache won't be used, even if it not empty
      # @option options [:unix, :windows, :mac] :linebreak_encoding the
      #   linebreak encoding of the result. If nothing is specified the result
      #   will be encoded as if :unix was specified.
      # @see Aef::Linebreak#encode
      def write(options = {})
        validate_options(options, :force_generation, :linebreak_encoding, :path)

        path = options[:path].nil? ? @path : to_pathname(options[:path])

        raise ArgumentError, 'No path given' unless path

        options.delete(:path)

        path.open('w') do |file|
          file.write(to_s(options))
        end

        true
      end

      # A String representation for debugging purposes.
      #
      # @return [String]
      def inspect
        generate_inspect(self, :path, :elements)
      end

      # A String representation of the hosts file.
      #
      # @param [Hash] options
      # @option options [true, false] :force_generation if set to true, the
      #   cache won't be used, even if it not empty
      # @option options [:unix, :windows, :mac] :linebreak_encoding the
      #   linebreak encoding of the result. If nothing is specified the result
      #   will be encoded as if :unix was specified.
      # @see Aef::Linebreak#encode
      def to_s(options = {})
        validate_options(options, :force_generation, :linebreak_encoding)

        string = ''

        @elements.each do |element|
          string << element.to_s(options)
        end

        string
      end

    end
  end
end