rpsl.rb 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. # Library which provides functions for reading and writing Routing Policy Specification Language (RPSL, RFC2622).
  2. module RPSL
  3. # Generic error when processing RPSL
  4. class RPSLError < StandardError; end
  5. # Error while loading RPSL
  6. class ParserError < RPSLError; end
  7. # Error while generating RPSL
  8. class DumpError < RPSLError; end
  9. # Parses a string as RPSL.
  10. # @param rpsl [String] The RPSL data as an object which can be iterated
  11. # with each_line.
  12. # @return [Hash] Hash containing the RPSL data.
  13. # @example
  14. # rpsl = <<EOF
  15. # route6: fd42:4242:2601:ffff::/64
  16. # descr: This is an ROA test object
  17. # max-length: 112
  18. # remarks:
  19. # This object is designed to test ROA scripts
  20. # in a number of ways.
  21. # +
  22. # The expected output for this route should be:
  23. # roa fd42:4242:2601:ffff::/64 max 64 as 0;
  24. # +
  25. # This is not a blank line
  26. # + ----- +
  27. # +
  28. # The first test is to include a number of syntax
  29. # corner cases, designed to trip up parsers
  30. # +
  31. # The first max-length line should be clamped
  32. # to the maximums defined in filter6.txt
  33. # +
  34. # The second max-length is part of the remark
  35. # and should be ignored.
  36. # +
  37. # max-length: 48
  38. # +
  39. # origin: AS0
  40. # mnt-by: BURBLE-MNT
  41. # source: DN42
  42. # EOF
  43. #
  44. # RPSL.load(rpsl) # => {"route6"=>"fd42:4242:2601:ffff::/64\n",
  45. # # "descr"=>"This is an ROA test object\n",
  46. # # "max-length"=>"112\n",
  47. # # "remarks"=>
  48. # # "\n" +
  49. # # "This object is designed to test ROA scripts\n" +
  50. # # "in a number of ways.\n" +
  51. # # "\n" +
  52. # # "The expected output for this route should be:\n" +
  53. # # "roa fd42:4242:2601:ffff::/64 max 64 as 0;\n" +
  54. # # "\n" +
  55. # # "This is not a blank line\n" +
  56. # # "+ ----- +\n" +
  57. # # "\n" +
  58. # # "The first test is to include a number of syntax\n" +
  59. # # "corner cases, designed to trip up parsers\n" +
  60. # # "\n" +
  61. # # "The first max-length line should be clamped\n" +
  62. # # "to the maximums defined in filter6.txt\n" +
  63. # # "\n" +
  64. # # "The second max-length is part of the remark\n" +
  65. # # "and should be ignored.\n" +
  66. # # "\n" +
  67. # # "max-length: 48\n" +
  68. # # "\n",
  69. # # "origin"=>"AS0\n",
  70. # # "mnt-by"=>"BURBLE-MNT\n",
  71. # # "source"=>"DN42\n"}
  72. def self.load(rpsl)
  73. obj = {}
  74. current_section = nil
  75. rpsl.to_s.each_line.with_index do |line, lineno|
  76. line.chomp!
  77. case line
  78. when /^([\d\w\-_]+): *(.*)$/
  79. current_section = $1
  80. obj[current_section] = "#{obj[current_section]}#{$2}\n"
  81. when /^[+ \s] *(.+)$/
  82. raise ParserError, "Value without key in line #{lineno + 1}" unless current_section
  83. obj[current_section] = "#{obj[current_section]}#{$1}\n"
  84. when /^\+ *$/
  85. raise ParserError, "Empty line without key in line #{lineno + 1}" unless current_section
  86. obj[current_section] = "#{obj[current_section]}\n"
  87. else
  88. raise ParserError, "Failed to parse line #{lineno + 1}."
  89. end
  90. end
  91. return obj
  92. end
  93. # Converts a hash into the RPSL format.
  94. # @param obj [Hash] Hash in which the data to be converted is stored.
  95. # @param value_column [Integer, NilClass] `nil` or the column where the
  96. # value should start.
  97. # @example
  98. # obj = {"route6"=>"fd42:4242:2601:ffff::/64",
  99. # "descr"=>"This is an ROA test object",
  100. # "max-length"=>"112",
  101. # "remarks"=>
  102. # "\n" +
  103. # "This object is designed to test ROA scripts\n" +
  104. # "in a number of ways.\n" +
  105. # "\n" +
  106. # "The expected output for this route should be:\n" +
  107. # "roa fd42:4242:2601:ffff::/64 max 64 as 0;\n" +
  108. # "\n" +
  109. # "This is not a blank line\n" +
  110. # "+ ----- +\n" +
  111. # "\n" +
  112. # "The first test is to include a number of syntax\n" +
  113. # "corner cases, designed to trip up parsers\n" +
  114. # "\n" +
  115. # "The first max-length line should be clamped\n" +
  116. # "to the maximums defined in filter6.txt\n" +
  117. # "\n" +
  118. # "The second max-length is part of the remark\n" +
  119. # "and should be ignored.\n" +
  120. # "\n" +
  121. # "max-length: 48",
  122. # "origin"=>"AS0",
  123. # "mnt-by"=>"BURBLE-MNT",
  124. # "source"=>"DN42"}
  125. #
  126. # RPSL.dump(obj, 20) # => "route6: fd42:4242:2601:ffff::/64\n" +
  127. # # "descr: This is an ROA test object\n" +
  128. # # "max-length: 112\n" +
  129. # # "remarks: \n" +
  130. # # " This object is designed to test ROA scripts\n" +
  131. # # " in a number of ways.\n" +
  132. # # "+\n" +
  133. # # " The expected output for this route should be:\n" +
  134. # # " roa fd42:4242:2601:ffff::/64 max 64 as 0;\n" +
  135. # # "+\n" +
  136. # # " This is not a blank line\n" +
  137. # # " + ----- +\n" +
  138. # # "+\n" +
  139. # # " The first test is to include a number of syntax\n" +
  140. # # " corner cases, designed to trip up parsers\n" +
  141. # # "+\n" +
  142. # # " The first max-length line should be clamped\n" +
  143. # # " to the maximums defined in filter6.txt\n" +
  144. # # "+\n" +
  145. # # " The second max-length is part of the remark\n" +
  146. # # " and should be ignored.\n" +
  147. # # "+\n" +
  148. # # " max-length: 48\n" +
  149. # # "origin: AS0\n" +
  150. # # "mnt-by: BURBLE-MNT\n" +
  151. # # "source: DN42\n"
  152. def self.dump(obj, value_column = nil)
  153. rpsl = ''
  154. obj.each do |key, value|
  155. lines = value.to_s.lines
  156. lines.map!(&:chomp)
  157. first_value = lines.shift
  158. key = key.to_s
  159. if value_column
  160. padding_length = value_column.to_i - 1 - key.length
  161. raise DumpError, 'Key too long.' if padding_length.negative?
  162. padding = ' ' * padding_length
  163. end
  164. rpsl += "#{key}:#{padding}#{first_value}\n"
  165. lines.each do |line|
  166. rpsl += if line.empty?
  167. "+\n"
  168. elsif value_column
  169. "#{' ' * value_column}#{line}\n"
  170. else
  171. " #{line}\n"
  172. end
  173. end
  174. end
  175. return rpsl
  176. end
  177. end