...

Source file src/github.com/sassoftware/relic/lib/binpatch/binpatch.go

Documentation: github.com/sassoftware/relic/lib/binpatch

     1  //
     2  // Copyright (c) SAS Institute Inc.
     3  //
     4  // Licensed under the Apache License, Version 2.0 (the "License");
     5  // you may not use this file except in compliance with the License.
     6  // You may obtain a copy of the License at
     7  //
     8  //     http://www.apache.org/licenses/LICENSE-2.0
     9  //
    10  // Unless required by applicable law or agreed to in writing, software
    11  // distributed under the License is distributed on an "AS IS" BASIS,
    12  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  // See the License for the specific language governing permissions and
    14  // limitations under the License.
    15  //
    16  
    17  // A means of conveying a series of edits to binary files. Each item in a
    18  // patchset consists of an offset into the old file, the number of bytes to
    19  // remove, and the octet string to replace it with.
    20  package binpatch
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/binary"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  
    30  	"github.com/sassoftware/relic/lib/atomicfile"
    31  )
    32  
    33  const (
    34  	MimeType = "application/x-binary-patch"
    35  
    36  	uint32Max = 0xffffffff
    37  )
    38  
    39  type PatchSet struct {
    40  	Patches []PatchHeader
    41  	Blobs   [][]byte
    42  }
    43  
    44  type PatchSetHeader struct {
    45  	Version, NumPatches uint32
    46  }
    47  
    48  type PatchHeader struct {
    49  	Offset           int64
    50  	OldSize, NewSize uint32
    51  }
    52  
    53  // Create a new, empty PatchSet
    54  func New() *PatchSet {
    55  	return new(PatchSet)
    56  }
    57  
    58  // Add a new patch region to a PatchSet. The bytes beginning at "offset" and
    59  // running for "oldSize" are removed and replaced with "blob". oldSize may be 0.
    60  func (p *PatchSet) Add(offset, oldSize int64, blob []byte) {
    61  	if len(p.Patches) > 0 {
    62  		i := len(p.Patches) - 1
    63  		last := p.Patches[i]
    64  		lastEnd := last.Offset + int64(last.OldSize)
    65  		lastBlob := p.Blobs[i]
    66  		oldCombo := int64(last.OldSize) + oldSize
    67  		newCombo := int64(len(lastBlob)) + int64(len(blob))
    68  		if offset == lastEnd && oldCombo <= uint32Max && newCombo <= uint32Max {
    69  			// coalesce this patch into the previous one
    70  			p.Patches[i].OldSize = uint32(oldCombo)
    71  			p.Patches[i].NewSize = uint32(newCombo)
    72  			if len(blob) > 0 {
    73  				newBlob := make([]byte, newCombo)
    74  				copy(newBlob, lastBlob)
    75  				copy(newBlob[len(lastBlob):], blob)
    76  				p.Blobs[i] = newBlob
    77  			}
    78  			return
    79  		}
    80  	}
    81  	for oldSize > uint32Max {
    82  		p.Patches = append(p.Patches, PatchHeader{offset, uint32Max, 0})
    83  		p.Blobs = append(p.Blobs, nil)
    84  		offset += uint32Max
    85  		oldSize -= uint32Max
    86  	}
    87  	p.Patches = append(p.Patches, PatchHeader{offset, uint32(oldSize), uint32(len(blob))})
    88  	p.Blobs = append(p.Blobs, blob)
    89  }
    90  
    91  // Unmarshal a PatchSet from bytes
    92  func Load(blob []byte) (*PatchSet, error) {
    93  	r := bytes.NewReader(blob)
    94  	var h PatchSetHeader
    95  	if err := binary.Read(r, binary.BigEndian, &h); err != nil {
    96  		return nil, err
    97  	} else if h.Version != 1 {
    98  		return nil, fmt.Errorf("unsupported binpatch version %d", h.Version)
    99  	}
   100  	num := int(h.NumPatches)
   101  	p := &PatchSet{
   102  		Patches: make([]PatchHeader, num),
   103  		Blobs:   make([][]byte, num),
   104  	}
   105  	if err := binary.Read(r, binary.BigEndian, p.Patches); err != nil {
   106  		return nil, err
   107  	}
   108  	for i, hdr := range p.Patches {
   109  		p.Blobs[i] = make([]byte, int(hdr.NewSize))
   110  		if _, err := io.ReadFull(r, p.Blobs[i]); err != nil {
   111  			return nil, err
   112  		}
   113  	}
   114  	return p, nil
   115  }
   116  
   117  // Marshal a PatchSet to bytes
   118  func (p *PatchSet) Dump() []byte {
   119  	header := PatchSetHeader{1, uint32(len(p.Patches))}
   120  	size := 8 + 16*len(p.Patches)
   121  	for _, hdr := range p.Patches {
   122  		size += int(hdr.NewSize)
   123  	}
   124  	buf := bytes.NewBuffer(make([]byte, 0, size))
   125  	binary.Write(buf, binary.BigEndian, header)
   126  	binary.Write(buf, binary.BigEndian, p.Patches)
   127  	for _, blob := range p.Blobs {
   128  		buf.Write(blob)
   129  	}
   130  	return buf.Bytes()
   131  }
   132  
   133  // Apply a PatchSet by taking the input file, transforming it, and writing the
   134  // result to outpath. If outpath is the same name as infile then the file will
   135  // be updated in-place if a direct overwrite is possible. If they are not the
   136  // same file, or the patch requires moving parts of the old file, then the
   137  // output will be written to a temporary file then renamed over the destination
   138  // path.
   139  func (p *PatchSet) Apply(infile *os.File, outpath string) error {
   140  	if outpath == "" {
   141  		outpath = infile.Name()
   142  	}
   143  	// Determine if an in-place overwrite is possible. If any test fails then
   144  	// fall back to doing a full copy (write-rename).
   145  	ininfo, err := infile.Stat()
   146  	if err != nil {
   147  		return p.applyRewrite(infile, outpath)
   148  	}
   149  	outinfo, err := os.Lstat(outpath)
   150  	if err != nil || !canOverwrite(ininfo, outinfo) {
   151  		return p.applyRewrite(infile, outpath)
   152  	}
   153  	size := ininfo.Size()
   154  	for i, patch := range p.Patches {
   155  		// All patches except the last must have oldsize == newsize
   156  		if patch.OldSize == patch.NewSize {
   157  			continue
   158  		} else if i != len(p.Patches)-1 {
   159  			return p.applyRewrite(infile, outpath)
   160  		}
   161  		// For the last patch, either oldsize == newsize or the patch must extend
   162  		// or truncate the file, i.e. the end of the old chunk must coincide
   163  		// with the end of the file.
   164  		oldEnd := patch.Offset + int64(patch.OldSize)
   165  		if oldEnd != ininfo.Size() {
   166  			return p.applyRewrite(infile, outpath)
   167  		}
   168  		size = patch.Offset + int64(patch.NewSize)
   169  	}
   170  	// Do in-place rewrite
   171  	for i, patch := range p.Patches {
   172  		if _, err := infile.WriteAt(p.Blobs[i], patch.Offset); err != nil {
   173  			return err
   174  		}
   175  	}
   176  	return infile.Truncate(size)
   177  }
   178  
   179  // Apply a patch by writing the patched result to a new file. This is the
   180  // fallback case whenever an in-place write isn't possible.
   181  func (p *PatchSet) applyRewrite(infile *os.File, outpath string) error {
   182  	if _, err := infile.Seek(0, 0); err != nil {
   183  		return err
   184  	}
   185  	outfile, err := atomicfile.New(outpath)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	defer outfile.Close()
   190  	var pos int64
   191  	for i, patch := range p.Patches {
   192  		blob := p.Blobs[i]
   193  		delta := patch.Offset - pos
   194  		if delta < 0 {
   195  			return errors.New("patches out of order")
   196  		}
   197  		// Copy data before the patch
   198  		if delta > 0 {
   199  			if _, err := io.CopyN(outfile, infile, delta); err != nil {
   200  				return err
   201  			}
   202  			pos += delta
   203  		}
   204  		// Skip the old data on the input file
   205  		delta = int64(patch.OldSize)
   206  		if _, err := infile.Seek(delta, io.SeekCurrent); err != nil {
   207  			return err
   208  		}
   209  		pos += delta
   210  		// Write the new data to the output file
   211  		if _, err := outfile.Write(blob); err != nil {
   212  			return err
   213  		}
   214  	}
   215  	// Copy everything after the last patch
   216  	if _, err := io.Copy(outfile, infile); err != nil {
   217  		return err
   218  	}
   219  	infile.Close()
   220  	return outfile.Commit()
   221  }
   222  
   223  func canOverwrite(ininfo, outinfo os.FileInfo) bool {
   224  	if !outinfo.Mode().IsRegular() {
   225  		return false
   226  	}
   227  	if !os.SameFile(ininfo, outinfo) {
   228  		return false
   229  	}
   230  	if hasLinks(outinfo) {
   231  		return false
   232  	}
   233  	return true
   234  }
   235  

View as plain text