package patch import ( "bytes"; "os"; ) type TextDiff []TextChunk // A TextChunk specifies an edit to a section of a file: // the text beginning at Line, which should be exactly Old, // is to be replaced with New. type TextChunk struct { Line int; Old []byte; New []byte; } func ParseTextDiff(raw []byte) (TextDiff, os.Error) { // Copy raw so it is safe to keep references to slices. _, chunks := sections(raw, "@@ -"); delta := 0; diff := make(TextDiff, len(chunks)); for i, raw := range chunks { c := &diff[i]; // Parse start line: @@ -oldLine,oldCount +newLine,newCount @@ junk chunk := splitLines(raw); chunkHeader := chunk[0]; var ok bool; var oldLine, oldCount, newLine, newCount int; s := chunkHeader; if oldLine, s, ok = atoi(s, "@@ -", 10); !ok { ErrChunkHdr: return nil, SyntaxError("unexpected chunk header line: " + string(chunkHeader)) } if len(s) == 0 || s[0] != ',' { oldCount = 1 } else if oldCount, s, ok = atoi(s, ",", 10); !ok { goto ErrChunkHdr } if newLine, s, ok = atoi(s, " +", 10); !ok { goto ErrChunkHdr } if len(s) == 0 || s[0] != ',' { newCount = 1 } else if newCount, s, ok = atoi(s, ",", 10); !ok { goto ErrChunkHdr } if !hasPrefix(s, " @@") { goto ErrChunkHdr } // Special case: for created or deleted files, the empty half // is given as starting at line 0. Translate to line 1. if oldCount == 0 && oldLine == 0 { oldLine = 1 } if newCount == 0 && newLine == 0 { newLine = 1 } // Count lines in text var dropOldNL, dropNewNL bool; var nold, nnew int; var lastch byte; chunk = chunk[1:len(chunk)]; for _, l := range chunk { if nold == oldCount && nnew == newCount && (len(l) == 0 || l[0] != '\\') { if len(bytes.TrimSpace(l)) != 0 { return nil, SyntaxError("too many chunk lines") } continue; } if len(l) == 0 { return nil, SyntaxError("empty chunk line") } switch l[0] { case '+': nnew++ case '-': nold++ case ' ': nnew++; nold++; case '\\': if _, ok := skip(l, "\\ No newline at end of file"); ok { switch lastch { case '-': dropOldNL = true case '+': dropNewNL = true case ' ': dropOldNL = true; dropNewNL = true; default: return nil, SyntaxError("message `\\ No newline at end of file' out of context") } break; } fallthrough; default: return nil, SyntaxError("unexpected chunk line: " + string(l)) } lastch = l[0]; } // Does it match the header? if nold != oldCount || nnew != newCount { return nil, SyntaxError("chunk header does not match line count: " + string(chunkHeader)) } if oldLine+delta != newLine { return nil, SyntaxError("chunk delta is out of sync with previous chunks") } delta += nnew - nold; c.Line = oldLine; var old, new bytes.Buffer; nold = 0; nnew = 0; for _, l := range chunk { if nold == oldCount && nnew == newCount { break } ch, l := l[0], l[1:len(l)]; if ch == '\\' { continue } if ch != '+' { old.Write(l); nold++; } if ch != '-' { new.Write(l); nnew++; } } c.Old = old.Bytes(); c.New = new.Bytes(); if dropOldNL { c.Old = c.Old[0 : len(c.Old)-1] } if dropNewNL { c.New = c.New[0 : len(c.New)-1] } } return diff, nil; } var ErrPatchFailure = os.NewError("patch did not apply cleanly") // Apply applies the changes listed in the diff // to the data, returning the new version. func (d TextDiff) Apply(data []byte) ([]byte, os.Error) { var buf bytes.Buffer; line := 1; for _, c := range d { var ok bool; var prefix []byte; prefix, data, ok = getLine(data, c.Line-line); if !ok || !bytes.HasPrefix(data, c.Old) { return nil, ErrPatchFailure } buf.Write(prefix); data = data[len(c.Old):len(data)]; buf.Write(c.New); line = c.Line + bytes.Count(c.Old, newline); } buf.Write(data); return buf.Bytes(), nil; }