From 957b39cba5d16836eda1fe8412fa164102e80cd8 Mon Sep 17 00:00:00 2001 From: Colin Cross Date: Wed, 21 Mar 2018 18:10:01 -0700 Subject: [PATCH] Add Patch and PatchList for making textual changes Patch and PatchList provide an API for making changes to substrings of a Blueprint file by parsing the AST and using the token positions to perform text replacements. Test: modify_test.go Change-Id: Ibb8993221982b54602ba5a05486198fab8d35a67 --- Blueprints | 1 + parser/modify.go | 70 ++++++++++++++++++++++++++++++++++++++++++- parser/modify_test.go | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 parser/modify_test.go diff --git a/Blueprints b/Blueprints index c892b46..93357fa 100644 --- a/Blueprints +++ b/Blueprints @@ -43,6 +43,7 @@ bootstrap_go_package { "parser/sort.go", ], testSrcs: [ + "parser/modify_test.go", "parser/parser_test.go", "parser/printer_test.go", ], diff --git a/parser/modify.go b/parser/modify.go index 08a3f3f..3051f66 100644 --- a/parser/modify.go +++ b/parser/modify.go @@ -14,7 +14,12 @@ package parser -import "fmt" +import ( + "fmt" + "io" + "math" + "sort" +) func AddStringToList(list *List, s string) (modified bool) { for _, v := range list.Values { @@ -50,3 +55,66 @@ func RemoveStringFromList(list *List, s string) (modified bool) { return false } + +// A Patch represents a region of a text buffer to be replaced [Start, End) and its Replacement +type Patch struct { + Start, End int + Replacement string +} + +// A PatchList is a list of sorted, non-overlapping Patch objects +type PatchList []Patch + +type PatchOverlapError error + +// Add adds a Patch to a PatchList. It returns a PatchOverlapError if the patch cannot be added. +func (list *PatchList) Add(start, end int, replacement string) error { + patch := Patch{start, end, replacement} + if patch.Start > patch.End { + return fmt.Errorf("invalid patch, start %d is after end %d", patch.Start, patch.End) + } + for _, p := range *list { + if (patch.Start >= p.Start && patch.Start < p.End) || + (patch.End >= p.Start && patch.End < p.End) || + (p.Start >= patch.Start && p.Start < patch.End) || + (p.Start == patch.Start && p.End == patch.End) { + return PatchOverlapError(fmt.Errorf("new patch %d-%d overlaps with existing patch %d-%d", + patch.Start, patch.End, p.Start, p.End)) + } + } + *list = append(*list, patch) + list.sort() + return nil +} + +func (list *PatchList) sort() { + sort.SliceStable(*list, + func(i, j int) bool { + return (*list)[i].Start < (*list)[j].Start + }) +} + +// Apply applies all the Patch objects in PatchList to the data from an input ReaderAt to an output Writer. +func (list *PatchList) Apply(in io.ReaderAt, out io.Writer) error { + var offset int64 + for _, patch := range *list { + toWrite := int64(patch.Start) - offset + written, err := io.Copy(out, io.NewSectionReader(in, offset, toWrite)) + if err != nil { + return err + } + offset += toWrite + if written != toWrite { + return fmt.Errorf("unexpected EOF at %d", offset) + } + + _, err = io.WriteString(out, patch.Replacement) + if err != nil { + return err + } + + offset += int64(patch.End - patch.Start) + } + _, err := io.Copy(out, io.NewSectionReader(in, offset, math.MaxInt64-offset)) + return err +} diff --git a/parser/modify_test.go b/parser/modify_test.go new file mode 100644 index 0000000..95fc293 --- /dev/null +++ b/parser/modify_test.go @@ -0,0 +1,65 @@ +// Copyright 2018 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "bytes" + "testing" +) + +func TestPatchList(t *testing.T) { + expectOverlap := func(err error) { + t.Helper() + if _, ok := err.(PatchOverlapError); !ok { + t.Error("missing PatchOverlapError") + } + } + + expectOk := func(err error) { + t.Helper() + if err != nil { + t.Error(err) + } + } + + in := []byte("abcdefghijklmnopqrstuvwxyz") + + patchlist := PatchList{} + expectOk(patchlist.Add(0, 3, "ABC")) + expectOk(patchlist.Add(12, 15, "MNO")) + expectOk(patchlist.Add(24, 26, "Z")) + expectOk(patchlist.Add(15, 15, "_")) + + expectOverlap(patchlist.Add(0, 3, "x")) + expectOverlap(patchlist.Add(12, 13, "x")) + expectOverlap(patchlist.Add(13, 14, "x")) + expectOverlap(patchlist.Add(14, 15, "x")) + expectOverlap(patchlist.Add(11, 13, "x")) + expectOverlap(patchlist.Add(12, 15, "x")) + expectOverlap(patchlist.Add(11, 15, "x")) + expectOverlap(patchlist.Add(15, 15, "x")) + + if t.Failed() { + return + } + + buf := new(bytes.Buffer) + patchlist.Apply(bytes.NewReader(in), buf) + expected := "ABCdefghijklMNO_pqrstuvwxZ" + got := buf.String() + if got != expected { + t.Errorf("expected %q, got %q", expected, got) + } +}