diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1cdd81f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + paths-ignore: + - 'docs/**' + - '*.md' + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Add Crystal repos + run: curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash + + - name: Install crystal + run: sudo apt install -y crystal libevent-dev libpcre3-dev libreadline-dev libssl-dev libyaml-dev + + - name: Install dependencies + run: shards install + + - name: Run tests + run: make test diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ffc7b6a..0000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: crystal diff --git a/spec/io_scanner_spec.cr b/spec/io_scanner_spec.cr index 989982e..0a17822 100644 --- a/spec/io_scanner_spec.cr +++ b/spec/io_scanner_spec.cr @@ -127,6 +127,19 @@ describe Fincher::IOScanner, "#rest" do end end +describe Fincher::IOScanner, "#gets_to_end" do + it "returns the remainder of the string from the offset" do + s = Fincher::IOScanner.new(::IO::Memory.new("this is a string")) + s.rest.should eq("this is a string") + + s.scan(/this is a /) + s.rest.should eq("string") + + s.scan(/string/) + s.rest.should eq("") + end +end + describe Fincher::IOScanner, "#[]" do it "allows access to subgroups of the last match" do s = Fincher::IOScanner.new(::IO::Memory.new("Fri Dec 12 1975 14:39")) diff --git a/spec/strategies/displacement/m_word_offset_spec.cr b/spec/strategies/displacement/m_word_offset_spec.cr index 492d582..cd8178d 100644 --- a/spec/strategies/displacement/m_word_offset_spec.cr +++ b/spec/strategies/displacement/m_word_offset_spec.cr @@ -11,10 +11,12 @@ describe Fincher::DisplacementStrategies::MWordOffset do 3 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem ipsum test blerg smorgasboorg")) + output_io = IO::Memory.new it "adds the configured offset to the StringScanner#offset" do - m_word_offsetter.advance_to_next!(source_text_scanner, Char::ZERO) + m_word_offsetter.advance_to_next!(source_text_scanner, Char::ZERO, io: output_io) source_text_scanner.offset.should eq(17) + output_io.to_s.should eq("lorem ipsum test ") end end @@ -25,10 +27,11 @@ describe Fincher::DisplacementStrategies::MWordOffset do 10 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem")) + output_io = IO::Memory.new it "raises an exception" do expect_raises(Fincher::StrategyNotFeasibleError) do - m_word_offsetter.advance_to_next!(source_text_scanner, Char::ZERO) + m_word_offsetter.advance_to_next!(source_text_scanner, Char::ZERO, io: output_io) end end end diff --git a/spec/strategies/displacement/matching_char_offset_spec.cr b/spec/strategies/displacement/matching_char_offset_spec.cr index f64db37..01a2541 100644 --- a/spec/strategies/displacement/matching_char_offset_spec.cr +++ b/spec/strategies/displacement/matching_char_offset_spec.cr @@ -11,10 +11,12 @@ describe Fincher::DisplacementStrategies::MatchingCharOffset do 3 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem ipsum test blerg smorgasboorg")) + output_io = IO::Memory.new it "adds the configured offset to the StringScanner#offset" do - matching_char_offsetter.advance_to_next!(source_text_scanner, 'r') + matching_char_offsetter.advance_to_next!(source_text_scanner, 'r', io: output_io) source_text_scanner.offset.should eq(20) + output_io.to_s.should eq("lorem ipsum test ble") end end @@ -25,10 +27,11 @@ describe Fincher::DisplacementStrategies::MatchingCharOffset do 10 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem")) + output_io = IO::Memory.new it "raises an exception" do expect_raises(Fincher::StrategyNotFeasibleError) do - matching_char_offsetter.advance_to_next!(source_text_scanner, 'r') + matching_char_offsetter.advance_to_next!(source_text_scanner, 'r', io: output_io) end end end diff --git a/spec/strategies/displacement/n_char_offset_spec.cr b/spec/strategies/displacement/n_char_offset_spec.cr index e32146d..aad6386 100644 --- a/spec/strategies/displacement/n_char_offset_spec.cr +++ b/spec/strategies/displacement/n_char_offset_spec.cr @@ -11,10 +11,12 @@ describe Fincher::DisplacementStrategies::NCharOffset do 10 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem ipsum test")) + output_io = IO::Memory.new it "adds the configured offset to the StringScanner#offset" do - n_char_offsetter.advance_to_next!(source_text_scanner, Char::ZERO) + n_char_offsetter.advance_to_next!(source_text_scanner, Char::ZERO, io: output_io) source_text_scanner.pos = 10 + output_io.to_s.should eq("lorem ipsu") end end @@ -25,10 +27,11 @@ describe Fincher::DisplacementStrategies::NCharOffset do 10 ) source_text_scanner = Fincher::IOScanner.new(IO::Memory.new("lorem")) + output_io = IO::Memory.new it "raises an exception" do expect_raises(Fincher::StrategyNotFeasibleError) do - n_char_offsetter.advance_to_next!(source_text_scanner, Char::ZERO) + n_char_offsetter.advance_to_next!(source_text_scanner, Char::ZERO, io: output_io) end end end diff --git a/spec/transformer_spec.cr b/spec/transformer_spec.cr index 93e9b10..0ef37ee 100644 --- a/spec/transformer_spec.cr +++ b/spec/transformer_spec.cr @@ -21,7 +21,7 @@ describe Fincher::Transformer do transformer.transform(transformed) transformed.to_s.should eq( - "I am a test sentenceh The quick brown foe jumps over the lazl dog. That dog is llzy as fuck. God damo. How many dogs there gotta be that be like this man come on son. Why." + "I am a test sentenceh The quick brown foxejumps over the lazy log. That dog is lazylas fuck. God damn. How many dogs there gotta be that be like this man come on son. Why." ) end end diff --git a/src/fincher/cli.cr b/src/fincher/cli.cr index acfc5b9..3c784b1 100644 --- a/src/fincher/cli.cr +++ b/src/fincher/cli.cr @@ -16,8 +16,8 @@ module Fincher @seed : UInt32? class Options - arg "source_text_file", required: true, desc: "source text file" arg "message", required: true, desc: "message" + arg "source_text_file", required: false, desc: "source text file. STDIN, if omitted" string "--seed", var: "NUMBER", required: false, @@ -57,7 +57,12 @@ module Fincher plaintext_scanner = ::IO::Memory.new(args.message) displacement_strategy = options.displacement_strategy replacement_strategy = options.replacement_strategy - source_file = File.open(args.source_text_file) + + source_file = if source_text_file = args.source_text_file? + File.open(source_text_file) + else + STDIN + end transformer = Fincher::Transformer.new( plaintext_scanner, @@ -122,7 +127,7 @@ module Fincher end def run - puts "Fincher #{version}" + puts "fincher #{version}" end end end diff --git a/src/fincher/io_scanner.cr b/src/fincher/io_scanner.cr index 97a4686..e450d5c 100644 --- a/src/fincher/io_scanner.cr +++ b/src/fincher/io_scanner.cr @@ -15,6 +15,10 @@ module Fincher def initialize(@io : Fincher::IO) end + def stdin? + io == STDIN + end + def [](index) @last_match.not_nil![index] end @@ -35,6 +39,10 @@ module Fincher rest_of_buffer + io.gets_to_end end + def gets_to_end + rest + end + def string @buffer + io.gets_to_end end @@ -58,6 +66,10 @@ module Fincher match(pattern, advance: false, options: Regex::Options::None) end + def skip(bytes_count : Int) + self.offset += bytes_count + end + def skip(pattern : Regex) match = scan(pattern) match.size if match @@ -81,11 +93,38 @@ module Fincher end def offset=(position) - raise IndexError.new if position < 0 || position >= size - @buffer_io_offset = position - io.pos = position - @eof_reached = false - next_buffer!(position) + raise IndexError.new if position < 0 + + buffer_io_offset = @buffer_io_offset + buffer_cursor = @buffer_cursor + buffer = @buffer + buffer_io_end_offset = buffer_io_offset + buffer.bytesize + + advance = (position - (buffer_io_offset + buffer_cursor)).to_i32 + + if position > buffer_io_offset && position < buffer_io_end_offset + @buffer_cursor += advance + elsif stdin? + # We cannot go backwards with STDIN + raise IndexError.new if position < buffer_io_offset + + @buffer_io_offset = position + + begin + io.skip(advance) + rescue ::IO::EOFError + # next_buffer! will handle EOF + end + + next_buffer!(position) + else + # We don't want to overrun + raise IndexError.new if position >= size + @buffer_io_offset = position + io.pos = position + @eof_reached = false + next_buffer!(position) + end end def pos @@ -96,6 +135,11 @@ module Fincher self.offset = position end + def print_buffer_position + puts @buffer + puts "#{" " * (Math.max(0, @buffer_cursor - 1))}^" + end + def size case _io = io when ::IO::FileDescriptor @@ -126,7 +170,7 @@ module Fincher end private def rest_of_buffer - @buffer[@buffer_cursor, buffer_size] + buffer[@buffer_cursor, buffer_size] end private def buffer_size @@ -182,15 +226,24 @@ module Fincher private def next_buffer!(anchor_position = nil) before_offset = anchor_position || offset - begin - buf = io.read_string(BUFFER_SIZE) - rescue ::IO::EOFError - io.pos = before_offset - buf = io.gets_to_end + buf = Bytes.new(BUFFER_SIZE) + bytes_read = io.read(buf) + + if bytes_read < BUFFER_SIZE @eof_reached = true end - @buffer_io_offset = io.pos - buf.bytesize + if bytes_read > 0 + buf = String.new(buf[0, bytes_read]) + else + buf = String.new + end + + @buffer_io_offset = if stdin? + before_offset + bytes_read + else + io.pos - bytes_read + end @buffer_cursor = 0 @buffer = buf buf diff --git a/src/fincher/strategies/displacement/base.cr b/src/fincher/strategies/displacement/base.cr index 56cb92d..06e4afc 100644 --- a/src/fincher/strategies/displacement/base.cr +++ b/src/fincher/strategies/displacement/base.cr @@ -7,7 +7,7 @@ module Fincher def initialize(@plaintext_scanner : Fincher::IO, @seed : UInt32) end - abstract def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char) : Fincher::IOScanner + abstract def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char, io : ::IO) : Fincher::IOScanner abstract def is_feasible?(scanner : Fincher::IOScanner) end diff --git a/src/fincher/strategies/displacement/m_word_offset.cr b/src/fincher/strategies/displacement/m_word_offset.cr index 55e4605..5cc9b17 100644 --- a/src/fincher/strategies/displacement/m_word_offset.cr +++ b/src/fincher/strategies/displacement/m_word_offset.cr @@ -6,9 +6,9 @@ module Fincher def initialize(@plaintext_scanner : Fincher::IO, @seed : UInt32, @offset : Int32) end - def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char) : Fincher::IOScanner + def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char, io : ::IO) : Fincher::IOScanner offset.times do - scanner.scan_until(/\b[\w\-\']+\b\W+\b/im) + io << scanner.scan_until(/\b[\w\-\']+\b\W+\b/im) raise StrategyNotFeasibleError.new( "Cannot advance #{offset} words at scanner position #{scanner.pos}" diff --git a/src/fincher/strategies/displacement/matching_char_offset.cr b/src/fincher/strategies/displacement/matching_char_offset.cr index f443d82..7def5b6 100644 --- a/src/fincher/strategies/displacement/matching_char_offset.cr +++ b/src/fincher/strategies/displacement/matching_char_offset.cr @@ -6,15 +6,20 @@ module Fincher def initialize(@plaintext_scanner : Fincher::IO, @seed : UInt32, @offset : Int32) end - def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char) : Fincher::IOScanner + def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char, io : IO) : Fincher::IOScanner offset.times do - scanner.scan_until(/\b[\w\-\']+\b\W+\b/im) + io << scanner.scan_until(/\b[\w\-\']+\b\W+\b/im) raise StrategyNotFeasibleError.new( "Cannot advance #{offset} words for character '#{msg_char}' at scanner position #{scanner.pos}" ) unless is_feasible?(scanner) end - scanner.skip_until(/#{msg_char}/i) + + matching_char_str = scanner.scan_until(/#{msg_char}/i) + raise StrategyNotFeasibleError.new( + "Cound not find character '#{msg_char}' after #{offset} words at scanner position #{scanner.pos}" + ) unless matching_char_str + io << matching_char_str[0...-1] scanner.offset = scanner.offset - 1 scanner end diff --git a/src/fincher/strategies/displacement/n_char_offset.cr b/src/fincher/strategies/displacement/n_char_offset.cr index ddfaf22..130ca28 100644 --- a/src/fincher/strategies/displacement/n_char_offset.cr +++ b/src/fincher/strategies/displacement/n_char_offset.cr @@ -6,17 +6,18 @@ module Fincher def initialize(@plaintext_scanner : Fincher::IO, @seed : UInt32, @offset : Int32) end - def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char) : Fincher::IOScanner + def advance_to_next!(scanner : Fincher::IOScanner, msg_char : Char, io : IO) : Fincher::IOScanner raise StrategyNotFeasibleError.new( "Cannot advance #{offset} chars at scanner position #{scanner.pos}" ) unless is_feasible?(scanner) - scanner.pos += offset + io << scanner.scan(/.{#{offset}}/) + scanner end def is_feasible?(scanner : Fincher::IOScanner) - scanner.size > scanner.pos + offset + scanner.stdin? || (scanner.size > scanner.pos + offset) end end end diff --git a/src/fincher/transformer.cr b/src/fincher/transformer.cr index 8a46cd7..76d55d4 100644 --- a/src/fincher/transformer.cr +++ b/src/fincher/transformer.cr @@ -21,26 +21,16 @@ module Fincher plaintext_scanner.each_char do |msg_char| # Advance position - displacer.advance_to_next!(source_scanner, msg_char) - - # Grab previously unmodified section - read_size = source_scanner.offset - current_offset - source_scanner.seek(current_offset) - - if read_size > 0 - unmodified = source_scanner.read_string(read_size) - io << unmodified - end + displacer.advance_to_next!(source_scanner, msg_char, io: io) # Replace the next char replaced_char = replacer.replace(msg_char) io << replaced_char - # Record this offset (+ 1 skipping the current char) - current_offset = source_scanner.offset + 1 + # Skip the current char, since we just replace it + source_scanner.skip(1) end - source_scanner.skip(1) io << source_scanner.gets_to_end io end