--- Special thanks to:
--- Bernd Schoeller (bernd dot schoeller from inf dot ethz dot ch)

---
--- file: application.e
---
indexing
    description : "Reads all standard input and tries to handle messages in each line."

class
    APPLICATION

create
    make

feature -- Initialization.

    make is
            -- Run application.
        local
            options_parsing_result: OPTIONS_PARSING_RESULT
        do
            options_parsing_result := (create {OPTION_PARSER}).parse
            if options_parsing_result.is_ok then
                options := options_parsing_result.options
                read_lines
            else
                io.put_string ("Invalid command line arguments: " +
                        options_parsing_result.reason)
                io.put_new_line
            end
        end

feature {NONE} -- Implementation.
    options: OPTIONS

    read_lines is
            -- Read lines from standard input and parses each.
        local
            input: KL_STDIN_FILE
        do
            create input.make
            from
                input.read_line
            until
                input.end_of_file
            loop
                parse_line( input.last_string )
                input.read_line
            end
        end

    parse_line( line: STRING ) is
            -- Parse and process one line.
        require
            line /= Void
        local
            line_parser: LINE_PARSER
            analyzer: HEADERS_AND_BODY_ANALYZER
            analyzing_result: ANALYZING_RESULT
        do
            create line_parser.make (line)
            if line_parser.is_message_extracted then
                create analyzer.make (line_parser.message)
                analyzing_result := analyzer.analyze
                if analyzing_result.is_good_message then
                    show_good_message_description (analyzing_result)
                else
                    show_problem_description (line, analyzing_result)
                end
            end
        end

    show_good_message_description (analyzing_result: ANALYZING_RESULT) is
            -- Shows description of good message if this is enabled.
        do
            if options.show_good_messages then
                io.put_string ("OK: ")
                io.put_string (analyzing_result.description)
                io.put_new_line
            end
        end

    show_problem_description (
            line: STRING;
            analyzing_result: ANALYZING_RESULT) is
                    -- Shows description of problematic message with
                    -- additional information.
        require
            line /= Void
            analyzing_result /= Void
        local
            additional_info: ADDITIONAL_INFO_EXTRACTOR
        do
            create additional_info.make (line)
            io.put_string ("ERROR: at [" + additional_info.timestamp +
                    "]; from: '" + additional_info.source_addr +
                    "'; to: '" + additional_info.dest_addr +
                    "'; " + analyzing_result.description)
            io.put_new_line
        end

end -- class APPLICATION

---
--- file: message_body.e
---
indexing
    description: "Message body container"

class
    MESSAGE_BODY

create
    make

feature -- Initialization
    make (raw_data: ARRAY [NATURAL_8]) is
            -- Creates objects with the data specified.
        do
            data := raw_data.twin
        end

feature -- Access
    data: ARRAY [NATURAL_8]
            -- Message body raw data.

    infix "@" (zero_based_index: INTEGER): NATURAL_8 is
            -- Returns Nth byte from body.
        require
            valid_index: zero_based_index >= 0 and zero_based_index < length
        do
            Result := data @ (data.lower + zero_based_index)
        end

    fragment (zero_based_start: INTEGER; size: INTEGER): ARRAY [NATURAL_8] is
            -- Returns body fragment.
            -- If size = 0 then returns empty array.
        require
            -- Note: in case when headers occupes all body index
            -- can be equal to body length.
            valid_start: zero_based_start >= 0 and zero_based_start <= length
            valid_length: zero_based_start + size <= length
            size >= 0
        do
            if 0 /= size then
                Result := data.subarray (data.lower + zero_based_start,
                        data.lower + zero_based_start + size - 1)
            else
                create Result.make (1, 0)
            end
        ensure
            Result /= Void
            Result.count = size
        end

    length: INTEGER is
            -- Message body length.
        do
            Result := data.count
        end
end

---
--- file: analyzing_result.e
---
indexing
    description: "Base class for results of message body analyzing"

deferred class
    ANALYZING_RESULT

feature
    is_good_message: BOOLEAN is
            -- Returns false if there is some problem with the message.
        deferred
        end

    description: STRING is
            -- Returns description for analyzing result.
        deferred
        ensure
            Result /= Void
        end
end

---
--- file: bad_result.e
---
indexing
    description: "Information about unsuccessful analyzing result."

class
    BAD_RESULT

inherit
    ANALYZING_RESULT
    HEX_TEXT_REPRESENTATION

create
    make

feature
    make (failure_reason: STRING; msg_full_body: ARRAY [NATURAL_8]) is
            -- Creates object with reason description and full
            -- message body.
        do
            reason := failure_reason.twin
            full_body := msg_full_body.twin
        end

    is_good_message: BOOLEAN is
            -- Always returns false.
        do
            Result := false
        end

    description: STRING is
            -- Returns reason and hex dump of message body.
        do
            Result := "What: " + reason + "; Full body: " +
                    binary_to_hex_text (full_body)
        end

feature {NONE}
    reason: STRING
    full_body: ARRAY [NATURAL_8]
end


---
--- file: header_element.e
---
indexing
    description: "GSM 03.40 Header Element"

class
    HEADER_ELEMENT

create
    make

feature
    make (element_id: NATURAL_8; element_data: ARRAY [NATURAL_8]) is
            -- Create header element with ID and DATA.
        require
            element_data /= Void
        do
            id := element_id
            data := element_data.twin
        end

    id: NATURAL_8
    data: ARRAY [NATURAL_8]
end

---
--- file: successful_result.e
---
indexing
    description: "Information about successful checking result"

class
    SUCCESSFUL_RESULT

inherit
    ANALYZING_RESULT
    HEX_TEXT_REPRESENTATION

create
    make_empty, make_with_header_elements_and_message

feature -- Creation
    make_empty is
            -- Creates result without header elements and message.
        do
            create {LINKED_LIST [HEADER_ELEMENT]}header_elements.make
            create message.make (1, 0)
        end

    make_with_header_elements_and_message (
            msg_header_elements: LIST [HEADER_ELEMENT];
            message_body: ARRAY [NATURAL_8]) is
                    -- Creates result with header elements and message body.
        require
            msg_header_elements /= Void
            message_body /= Void
        do
            header_elements := msg_header_elements
            message := message_body
        end

feature -- Queries
    is_good_message: BOOLEAN is
            -- Always returns true.
        do
            Result := true
        end

    description: STRING is
            -- Returns text representation of header elements and body.
        do
            create Result.make_empty
            from
                header_elements.start
            until
                header_elements.after
            loop
                Result.append ("Element: Id: " +
                        (header_elements.item).id.to_hex_string +
                        ", Data: " +
                        binary_to_hex_text ((header_elements.item).data) +
                        "; ")
                header_elements.forth
            end

            Result.append ("Message: " + binary_to_hex_text (message))
        end

feature {NONE} -- Implementation
    header_elements: LIST [HEADER_ELEMENT]
    message: ARRAY [NATURAL_8]
end

---
--- file: headers_and_body_analyzer.e
---
indexing
    description: "Analyzer of message headers and body"

class
    HEADERS_AND_BODY_ANALYZER

create
    make

feature -- Initialization
    make (message_body: MESSAGE_BODY) is
            -- Initializes analyzer.
        do
            body := message_body
        end

feature -- Main functionality
    analyze: ANALYZING_RESULT is
            -- Analyzes binary image for headers and body errors.
        do
            if 0 = body.length then
                -- Empty body is correct.
                create {SUCCESSFUL_RESULT}Result.make_empty
            else
                Result := check_header_and_body
            end
        ensure
            Result /= Void
        end

feature {NONE} -- Implementation
    body: MESSAGE_BODY

    check_header_and_body: ANALYZING_RESULT is
            -- Checks headers and body on non-empty message.
        require
            non_empty_message: body.length > 0
        local
            header_length: INTEGER
        do
            header_length := (body @ 0).to_integer_32

            if 0 = header_length then
                create {BAD_RESULT}Result.make ("Zero header length!", body.data)
            elseif header_length >= body.length then
                Result := result_header_length_too_big (header_length)
            else
                Result := check_header_elements_and_body (header_length)
            end
        end

    check_header_elements_and_body (
            header_length: INTEGER): ANALYZING_RESULT is
            -- Checks header elements and body.
        require
            body.length > 0
            header_length > 0
            body.length > header_length
        local
            elements: LINKED_LIST [HEADER_ELEMENT]
            i: INTEGER
            element_id, element_size: NATURAL_8
            bad_element: BOOLEAN
            next_element: HEADER_ELEMENT
        do
            create elements.make
            from
                i := 0
                bad_element := false
            variant
                header_length - i
            until
                i + 2 >= header_length or bad_element
            loop
                element_id := body @ (1 + i); i := i + 1
                element_size := body @ (1 + i); i := i + 1
                if element_size < header_length + 1 - i then
                    create next_element.make (
                            element_id,
                            body.fragment (1 + i, element_size))
                    check next_element.data.count = element_size end
                    elements.put_right (next_element)
                    elements.forth
                    i := i + element_size
                else
                    bad_element := true
                    Result := result_element_size_too_big (
                            1 + i,
                            header_length,
                            element_size)
                end
            end
            if not bad_element then
                Result := check_message_body_and_make_final_result (
                        header_length,
                        elements)
            end
        end

    check_message_body_and_make_final_result (
            header_length: INTEGER;
            elements: LIST [HEADER_ELEMENT]): ANALYZING_RESULT is
            -- Check correcteness of message and make the final analyzing result.
        local
            message: ARRAY [NATURAL_8]
            i: INTEGER
            bad_byte: BOOLEAN
        do
            message := body.fragment(
                    header_length + 1,
                    body.length - header_length - 1)
            from
                i := message.lower
                bad_byte := false
            variant
                message.upper + 1 - i
            until
                i > message.upper or bad_byte
            loop
                if 0 /= (message @ i).bit_and (0x80) then
                    bad_byte := true
                    create {BAD_RESULT}Result.make (
                            "Byte with non-zero highest bit in message! Byte: " +
                                    (message @ i).to_hex_string,
                            body.data)
                end
                i := i + 1
            end
            if not bad_byte then
                create {SUCCESSFUL_RESULT}Result.make_with_header_elements_and_message (
                        elements, message)
            end
        end

    result_header_length_too_big (
            header_length: INTEGER): BAD_RESULT is
            -- Creates BAD_RESULT object with appropriate message.
        do
            create Result.make (
                    "Header length too big! Header length: " +
                    formatter.formatted (header_length) +
                    ", Body length: " +
                    formatter.formatted (body.length),
                    body.data)
        end

    result_element_size_too_big (
            position: INTEGER;
            header_length: INTEGER;
            element_size: INTEGER): BAD_RESULT is
            -- Creates BAD_RESULT object with appropriate message.
        do
            create Result.make (
                    "Invalid element size! Position: " +
                            formatter.formatted (position) +
                            ", HeaderLength: " +
                            formatter.formatted (header_length) +
                            ", ElementSize: " +
                            formatter.formatted (element_size),
                    body.data)
        end

    formatter: FORMAT_INTEGER is
            -- Returns formatter for INTEGER objects.
        once
            create Result.make (1)
        end
end

---
--- file: line_parser.e
---
indexing
    description: "Input line parser"

class
    LINE_PARSER

create
    make

feature -- Initialization
    make (line: STRING) is
            -- Tries to parse line and extract message body.
        local
            body_image: ARRAY [NATURAL_8]
        do
            if line.count > 2 then
                body_image := extract_short_message_body (line)
                if body_image /= Void then
                    create extracted_message.make (body_image)
                end
            end
        end

feature -- Main functionality
    is_message_extracted: BOOLEAN is
            -- Returns true if line could be parsed.
        do
            Result := extracted_message /= Void
        end

    message: MESSAGE_BODY is
            -- Accessor for extracted message body.
        require
            is_message_extracted
        do
            Result := extracted_message
        ensure
            Result /= Void
        end

feature {NONE} -- Implementation
    extracted_message: MESSAGE_BODY
            -- Body of extracted message.
            -- Equal to Void if message doesn't extracted.

    extract_short_message_body (line: STRING): ARRAY [NATURAL_8] is
            -- Try to extract message body from the current line.
        require
            line /= Void
        do
            short_message_body_regex.match (line)
            if short_message_body_regex.has_matched then
                Result := string_image_to_binary (
                        short_message_body_regex.captured_substring (1))
            else
                tlv_message_payload_regex.match (line)
                if tlv_message_payload_regex.has_matched then
                    Result := hex_image_to_binary (
                            tlv_message_payload_regex.captured_substring (1))
                end
            end
        end

    short_message_body_regex: RX_PCRE_REGULAR_EXPRESSION is
            -- Returns regular expression for matching short_message body.
        once
            create Result.make
            Result.compile ("\{short_message %"(.+)%" }")
            check
                Result.is_compiled
            end
        ensure
            Result.is_compiled
        end

    tlv_message_payload_regex: RX_PCRE_REGULAR_EXPRESSION is
            -- Returns regular expression for matching 'message payload TLV'.
        once
            create Result.make
            Result.compile ("\{tlv \{t 0x424} \{l 0x[0-9A-Fa-f]+} \{v ([^}]+)}")
        ensure
            Result.is_compiled
        end

    string_image_to_binary (string_image: STRING): ARRAY [NATURAL_8] is
            -- Converts string image representation to binary image.
        require
            string_image /= Void
        local
            string_without_escapes: STRING
            i: INTEGER
        do
            string_without_escapes := unescape_string_content (
                    unescape_string_content (string_image))
            create Result.make (1, string_without_escapes.count)
            from
                i := 1
            variant
                string_without_escapes.count + 1 - i
            until
                i > string_without_escapes.count
            loop
                Result.put (string_without_escapes.item_code (i).to_natural_8, i)
                i := i + 1
            end
        ensure
            Result /= Void
        end

    hex_image_to_binary (string_image: STRING): ARRAY [NATURAL_8] is
            -- Converts image in the form 'xx xx xx xx...' to binary.
        require
            string_image /= Void
        local
            hex_images: LIST [STRING]
            i: INTEGER
        do
            hex_images := string_image.split (' ')
            create Result.make (1, hex_images.count)

            from
                hex_images.start
                i := 1
            until
                hex_images.after
            loop
                Result.put (byte_from_hex (hex_images.item), i)
                i := i + 1
                hex_images.forth
            end
        ensure
            Result /= Void
        end

    unescape_string_content (string_image: STRING): STRING is
            -- Part of conversion implementation process.
            -- Changes escape sequence into characters.
        local
            ch: CHARACTER_8
            i: INTEGER
        do
            create Result.make_empty

            from
                i := 1
            variant
                string_image.count + 1 - i
            until
                i > string_image.count
            loop
                ch := string_image @ i
                if '\' = ch then
                    i := i + 1
                    ch := string_image @ i
                    inspect ch
                    when 'n' then ch := '%N'
                    when 'r' then ch := '%R'
                    when 't' then ch := '%T'
                    when 'x' then
                        ch := byte_from_hex (string_image.substring (i + 1, i + 2)).to_character_8
                        i := i + 2
                    else
                        -- Remain character unchanged.
                    end
                end
                Result.append_character( ch )
                i := i + 1
            end
        ensure
            Result /= Void
        end

    byte_from_hex (hex: STRING_8): NATURAL_8 is
            -- Convert 'xx' string to int8.
        require
            hex.count = 2 or hex.count = 1
        local
            i: INTEGER
        do
            Result := 0
            from
                i := 1
            variant
                hex.count + 1 - i
            until
                i > hex.count
            loop
                Result := (Result * 16 + hexadecimal_characters.index_of ((hex @ i).upper, 1) - 1).to_natural_8
                i := i + 1
            end
        end

    hexadecimal_characters: STRING_8 is "0123456789ABCDEF"
end

---
--- file: additional_info_extractor.e
---
indexing
    description: "Helper object for extraction timestamp, source and dest addresses"

class
    ADDITIONAL_INFO_EXTRACTOR

create
    make

feature -- Initialization
    make (line: STRING) is
            -- Initializes objects by parsing the line specified.
        require
            line /= Void
        do
            additional_info_regex.match (line)
            if additional_info_regex.has_matched then
                timestamp := additional_info_regex.captured_substring (1)
                source_addr := additional_info_regex.captured_substring (2)
                dest_addr := additional_info_regex.captured_substring (3)
            else
                (create {EXCEPTIONS}).raise ("Unable to parse line for additional info")
            end
        end

    timestamp: STRING
    source_addr: STRING
    dest_addr: STRING

feature {NONE} -- Implementation
    additional_info_regex: RX_PCRE_REGULAR_EXPRESSION is
            -- Returns regex for additional info extraction.
        once
            create Result.make
            Result.compile(
                    "^LOG \[([^]]+)\].*\{source_addr %"([^%"]+)%" }.*\{dest_addr %"([^%"]+)%" }" )
        ensure
            Result /= Void
            Result.is_compiled
        end

end

---
--- file: hex_text_representation.e
---
indexing
    description: "Impementation of binary data to text transformation."

class
    HEX_TEXT_REPRESENTATION

feature
    binary_to_hex_text (bin_image: ARRAY [NATURAL_8] ): STRING is
            -- Converts binary data to string in format 'xx xx xx...'.
        require
            bin_image /= Void
        local
            i: INTEGER
        do
            create Result.make (bin_image.upper * 3)
            from
                i := bin_image.lower
            variant
                bin_image.upper + 1 - i
            until
                i > bin_image.upper
            loop
                if i > bin_image.lower then
                    Result.append (" ")
                end
                Result.append ((bin_image @ i).to_hex_string)
                i := i + 1
            end
        end
end

---
--- file: options.e
---
indexing
    description: "Program options container"

class
    OPTIONS

feature -- Access
    show_good_messages: BOOLEAN
    set_show_good_messages( value: BOOLEAN ) is
        do
            show_good_messages := value
        end
end

---
--- file: options_parsing_result.e
---
indexing
    description: "Command line options parsing result"

class
    OPTIONS_PARSING_RESULT

create
    make_ok,
    make_failed

feature -- Initialization
    make_ok (parsed_options: OPTIONS) is
            -- Creation when parsing successful.
        do
            is_ok := true
            options := parsed_options
        end

    make_failed( failure_reason: STRING ) is
            -- Creation when parsing failed.
        require
            failure_reason /= Void
        do
            is_ok := false
            reason := failure_reason.twin
        end

feature -- Access
    is_ok: BOOLEAN
    options: OPTIONS
    reason: STRING

invariant
    options_iff_ok: is_ok implies (options /= Void)
    reason_iff_failure: (not is_ok) implies ((reason /= Void) and (options = Void))
end

---
--- file: option_parser.e
---
indexing
    description: "Command line option parser"

class
    OPTION_PARSER

inherit
    ARGUMENTS

feature -- Commands
    parse: OPTIONS_PARSING_RESULT is
            -- Parse command line options.
        local
            options: OPTIONS
            reason: STRING
            i: INTEGER
            show_good_messages: BOOLEAN
        do
            from
                i := 1
            until
                (i > argument_count) or (reason /= Void)
            loop
                if ("--show-good").is_equal (argument (i)) then
                    show_good_messages := true
                else
                    reason := "Unsupported argument: " + argument (i)
                end
                i := i + 1
            end

            if reason /= Void then
                create Result.make_failed (reason)
            else
                create options
                options.set_show_good_messages (show_good_messages)
                create Result.make_ok (options)
            end
        ensure
            Result /= Void
        end
end

-- vim:ts=4:sts=4:sw=4:
Hosted by uCoz