Initial import of stuff I don't hate

This commit is contained in:
Paul Betts
2014-07-28 08:37:01 +02:00
parent 7e4771c334
commit 8fa1cf703c
16 changed files with 2505 additions and 0 deletions

899
src/BinaryPatchUtility.cs Normal file
View File

@@ -0,0 +1,899 @@
using System;
using System.IO;
using Ionic.BZip2;
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
namespace Squirrel.Core
{
/*
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
distributed under the following license:
Copyright 2003-2005 Colin Percival
All rights reserved
Redistribution and use in source and binary forms, with or without
modification, are permitted providing that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class BinaryPatchUtility
{
/// <summary>
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
/// (by <see cref="Apply"/>) to transform <paramref name="oldData"/> into <paramref name="newData"/>.
/// </summary>
/// <param name="oldData">The original binary data.</param>
/// <param name="newData">The new binary data.</param>
/// <param name="output">A <see cref="Stream"/> to which the patch will be written.</param>
public static void Create(byte[] oldData, byte[] newData, Stream output)
{
// check arguments
if (oldData == null)
throw new ArgumentNullException("oldData");
if (newData == null)
throw new ArgumentNullException("newData");
if (output == null)
throw new ArgumentNullException("output");
if (!output.CanSeek)
throw new ArgumentException("Output stream must be seekable.", "output");
if (!output.CanWrite)
throw new ArgumentException("Output stream must be writable.", "output");
/* Header is
0 8 "BSDIFF40"
8 8 length of bzip2ed ctrl block
16 8 length of bzip2ed diff block
24 8 length of new file */
/* File is
0 32 Header
32 ?? Bzip2ed ctrl block
?? ?? Bzip2ed diff block
?? ?? Bzip2ed extra block */
byte[] header = new byte[c_headerSize];
WriteInt64(c_fileSignature, header, 0); // "BSDIFF40"
WriteInt64(0, header, 8);
WriteInt64(0, header, 16);
WriteInt64(newData.Length, header, 24);
long startPosition = output.Position;
output.Write(header, 0, header.Length);
int[] I = SuffixSort(oldData);
byte[] db = new byte[newData.Length + 1];
byte[] eb = new byte[newData.Length + 1];
int dblen = 0;
int eblen = 0;
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
// compute the differences, writing ctrl as we go
int scan = 0;
int pos = 0;
int len = 0;
int lastscan = 0;
int lastpos = 0;
int lastoffset = 0;
while (scan < newData.Length)
{
int oldscore = 0;
for (int scsc = scan += len; scan < newData.Length; scan++)
{
len = Search(I, oldData, newData, scan, 0, oldData.Length, out pos);
for (; scsc < scan + len; scsc++)
{
if ((scsc + lastoffset < oldData.Length) && (oldData[scsc + lastoffset] == newData[scsc]))
oldscore++;
}
if ((len == oldscore && len != 0) || (len > oldscore + 8))
break;
if ((scan + lastoffset < oldData.Length) && (oldData[scan + lastoffset] == newData[scan]))
oldscore--;
}
if (len != oldscore || scan == newData.Length)
{
int s = 0;
int sf = 0;
int lenf = 0;
for (int i = 0; (lastscan + i < scan) && (lastpos + i < oldData.Length); )
{
if (oldData[lastpos + i] == newData[lastscan + i])
s++;
i++;
if (s * 2 - i > sf * 2 - lenf)
{
sf = s;
lenf = i;
}
}
int lenb = 0;
if (scan < newData.Length)
{
s = 0;
int sb = 0;
for (int i = 1; (scan >= lastscan + i) && (pos >= i); i++)
{
if (oldData[pos - i] == newData[scan - i])
s++;
if (s * 2 - i > sb * 2 - lenb)
{
sb = s;
lenb = i;
}
}
}
if (lastscan + lenf > scan - lenb)
{
int overlap = (lastscan + lenf) - (scan - lenb);
s = 0;
int ss = 0;
int lens = 0;
for (int i = 0; i < overlap; i++)
{
if (newData[lastscan + lenf - overlap + i] == oldData[lastpos + lenf - overlap + i])
s++;
if (newData[scan - lenb + i] == oldData[pos - lenb + i])
s--;
if (s > ss)
{
ss = s;
lens = i + 1;
}
}
lenf += lens - overlap;
lenb -= lens;
}
for (int i = 0; i < lenf; i++)
db[dblen + i] = (byte)(newData[lastscan + i] - oldData[lastpos + i]);
for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++)
eb[eblen + i] = newData[lastscan + lenf + i];
dblen += lenf;
eblen += (scan - lenb) - (lastscan + lenf);
byte[] buf = new byte[8];
WriteInt64(lenf, buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((scan - lenb) - (lastscan + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((pos - lenb) - (lastpos + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
lastscan = scan - lenb;
lastpos = pos - lenb;
lastoffset = pos - scan;
}
}
}
// compute size of compressed ctrl data
long controlEndPosition = output.Position;
WriteInt64(controlEndPosition - startPosition - c_headerSize, header, 8);
// write compressed diff data
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
bz2Stream.Write(db, 0, dblen);
}
// compute size of compressed diff data
long diffEndPosition = output.Position;
WriteInt64(diffEndPosition - controlEndPosition, header, 16);
// write compressed extra data
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
bz2Stream.Write(eb, 0, eblen);
}
// seek to the beginning, write the header, then seek back to end
long endPosition = output.Position;
output.Position = startPosition;
output.Write(header, 0, header.Length);
output.Position = endPosition;
}
/// <summary>
/// Applies a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) to the data in
/// <paramref name="input"/> and writes the results of patching to <paramref name="output"/>.
/// </summary>
/// <param name="input">A <see cref="Stream"/> containing the input data.</param>
/// <param name="openPatchStream">A func that can open a <see cref="Stream"/> positioned at the start of the patch data.
/// This stream must support reading and seeking, and <paramref name="openPatchStream"/> must allow multiple streams on
/// the patch to be opened concurrently.</param>
/// <param name="output">A <see cref="Stream"/> to which the patched data is written.</param>
public static void Apply(Stream input, Func<Stream> openPatchStream, Stream output)
{
// check arguments
if (input == null)
throw new ArgumentNullException("input");
if (openPatchStream == null)
throw new ArgumentNullException("openPatchStream");
if (output == null)
throw new ArgumentNullException("output");
/*
File format:
0 8 "BSDIFF40"
8 8 X
16 8 Y
24 8 sizeof(newfile)
32 X bzip2(control block)
32+X Y bzip2(diff block)
32+X+Y ??? bzip2(extra block)
with control block a set of triples (x,y,z) meaning "add x bytes
from oldfile to x bytes from the diff block; copy y bytes from the
extra block; seek forwards in oldfile by z bytes".
*/
// read header
long controlLength, diffLength, newSize;
using (Stream patchStream = openPatchStream())
{
// check patch stream capabilities
if (!patchStream.CanRead)
throw new ArgumentException("Patch stream must be readable.", "openPatchStream");
if (!patchStream.CanSeek)
throw new ArgumentException("Patch stream must be seekable.", "openPatchStream");
byte[] header = patchStream.ReadExactly(c_headerSize);
// check for appropriate magic
long signature = ReadInt64(header, 0);
if (signature != c_fileSignature)
throw new InvalidOperationException("Corrupt patch.");
// read lengths from header
controlLength = ReadInt64(header, 8);
diffLength = ReadInt64(header, 16);
newSize = ReadInt64(header, 24);
if (controlLength < 0 || diffLength < 0 || newSize < 0)
throw new InvalidOperationException("Corrupt patch.");
}
// preallocate buffers for reading and writing
const int c_bufferSize = 1048576;
byte[] newData = new byte[c_bufferSize];
byte[] oldData = new byte[c_bufferSize];
// prepare to read three parts of the patch in parallel
using (Stream compressedControlStream = openPatchStream())
using (Stream compressedDiffStream = openPatchStream())
using (Stream compressedExtraStream = openPatchStream())
{
// seek to the start of each part
compressedControlStream.Seek(c_headerSize, SeekOrigin.Current);
compressedDiffStream.Seek(c_headerSize + controlLength, SeekOrigin.Current);
compressedExtraStream.Seek(c_headerSize + controlLength + diffLength, SeekOrigin.Current);
// decompress each part (to read it)
using (BZip2InputStream controlStream = new BZip2InputStream(compressedControlStream))
using (BZip2InputStream diffStream = new BZip2InputStream(compressedDiffStream))
using (BZip2InputStream extraStream = new BZip2InputStream(compressedExtraStream))
{
long[] control = new long[3];
byte[] buffer = new byte[8];
int oldPosition = 0;
int newPosition = 0;
while (newPosition < newSize)
{
// read control data
for (int i = 0; i < 3; i++)
{
controlStream.ReadExactly(buffer, 0, 8);
control[i] = ReadInt64(buffer, 0);
}
// sanity-check
if (newPosition + control[0] > newSize)
throw new InvalidOperationException("Corrupt patch.");
// seek old file to the position that the new data is diffed against
input.Position = oldPosition;
int bytesToCopy = (int)control[0];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
// read diff string
diffStream.ReadExactly(newData, 0, actualBytesToCopy);
// add old data to diff string
int availableInputBytes = Math.Min(actualBytesToCopy, (int)(input.Length - input.Position));
input.ReadExactly(oldData, 0, availableInputBytes);
for (int index = 0; index < availableInputBytes; index++)
newData[index] += oldData[index];
output.Write(newData, 0, actualBytesToCopy);
// adjust counters
newPosition += actualBytesToCopy;
oldPosition += actualBytesToCopy;
bytesToCopy -= actualBytesToCopy;
}
// sanity-check
if (newPosition + control[1] > newSize)
throw new InvalidOperationException("Corrupt patch.");
// read extra string
bytesToCopy = (int)control[1];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
extraStream.ReadExactly(newData, 0, actualBytesToCopy);
output.Write(newData, 0, actualBytesToCopy);
newPosition += actualBytesToCopy;
bytesToCopy -= actualBytesToCopy;
}
// adjust position
oldPosition = (int)(oldPosition + control[2]);
}
}
}
}
private static int CompareBytes(byte[] left, int leftOffset, byte[] right, int rightOffset)
{
for (int index = 0; index < left.Length - leftOffset && index < right.Length - rightOffset; index++)
{
int diff = left[index + leftOffset] - right[index + rightOffset];
if (diff != 0)
return diff;
}
return 0;
}
private static int MatchLength(byte[] oldData, int oldOffset, byte[] newData, int newOffset)
{
int i;
for (i = 0; i < oldData.Length - oldOffset && i < newData.Length - newOffset; i++)
{
if (oldData[i + oldOffset] != newData[i + newOffset])
break;
}
return i;
}
private static int Search(int[] I, byte[] oldData, byte[] newData, int newOffset, int start, int end, out int pos)
{
if (end - start < 2)
{
int startLength = MatchLength(oldData, I[start], newData, newOffset);
int endLength = MatchLength(oldData, I[end], newData, newOffset);
if (startLength > endLength)
{
pos = I[start];
return startLength;
}
else
{
pos = I[end];
return endLength;
}
}
else
{
int midPoint = start + (end - start) / 2;
return CompareBytes(oldData, I[midPoint], newData, newOffset) < 0 ?
Search(I, oldData, newData, newOffset, midPoint, end, out pos) :
Search(I, oldData, newData, newOffset, start, midPoint, out pos);
}
}
private static void Split(int[] I, int[] v, int start, int len, int h)
{
if (len < 16)
{
int j;
for (int k = start; k < start + len; k += j)
{
j = 1;
int x = v[I[k] + h];
for (int i = 1; k + i < start + len; i++)
{
if (v[I[k + i] + h] < x)
{
x = v[I[k + i] + h];
j = 0;
}
if (v[I[k + i] + h] == x)
{
Swap(ref I[k + j], ref I[k + i]);
j++;
}
}
for (int i = 0; i < j; i++)
v[I[k + i]] = k + j - 1;
if (j == 1)
I[k] = -1;
}
}
else
{
int x = v[I[start + len / 2] + h];
int jj = 0;
int kk = 0;
for (int i2 = start; i2 < start + len; i2++)
{
if (v[I[i2] + h] < x)
jj++;
if (v[I[i2] + h] == x)
kk++;
}
jj += start;
kk += jj;
int i = start;
int j = 0;
int k = 0;
while (i < jj)
{
if (v[I[i] + h] < x)
{
i++;
}
else if (v[I[i] + h] == x)
{
Swap(ref I[i], ref I[jj + j]);
j++;
}
else
{
Swap(ref I[i], ref I[kk + k]);
k++;
}
}
while (jj + j < kk)
{
if (v[I[jj + j] + h] == x)
{
j++;
}
else
{
Swap(ref I[jj + j], ref I[kk + k]);
k++;
}
}
if (jj > start)
Split(I, v, start, jj - start, h);
for (i = 0; i < kk - jj; i++)
v[I[jj + i]] = kk - 1;
if (jj == kk - 1)
I[jj] = -1;
if (start + len > kk)
Split(I, v, kk, start + len - kk, h);
}
}
private static int[] SuffixSort(byte[] oldData)
{
int[] buckets = new int[256];
foreach (byte oldByte in oldData)
buckets[oldByte]++;
for (int i = 1; i < 256; i++)
buckets[i] += buckets[i - 1];
for (int i = 255; i > 0; i--)
buckets[i] = buckets[i - 1];
buckets[0] = 0;
int[] I = new int[oldData.Length + 1];
for (int i = 0; i < oldData.Length; i++)
I[++buckets[oldData[i]]] = i;
int[] v = new int[oldData.Length + 1];
for (int i = 0; i < oldData.Length; i++)
v[i] = buckets[oldData[i]];
for (int i = 1; i < 256; i++)
{
if (buckets[i] == buckets[i - 1] + 1)
I[buckets[i]] = -1;
}
I[0] = -1;
for (int h = 1; I[0] != -(oldData.Length + 1); h += h)
{
int len = 0;
int i = 0;
while (i < oldData.Length + 1)
{
if (I[i] < 0)
{
len -= I[i];
i -= I[i];
}
else
{
if (len != 0)
I[i - len] = -len;
len = v[I[i]] + 1 - i;
Split(I, v, i, len, h);
i += len;
len = 0;
}
}
if (len != 0)
I[i - len] = -len;
}
for (int i = 0; i < oldData.Length + 1; i++)
I[v[i]] = i;
return I;
}
private static void Swap(ref int first, ref int second)
{
int temp = first;
first = second;
second = temp;
}
private static long ReadInt64(byte[] buf, int offset)
{
long value = buf[offset + 7] & 0x7F;
for (int index = 6; index >= 0; index--)
{
value *= 256;
value += buf[offset + index];
}
if ((buf[offset + 7] & 0x80) != 0)
value = -value;
return value;
}
private static void WriteInt64(long value, byte[] buf, int offset)
{
long valueToWrite = value < 0 ? -value : value;
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
{
buf[offset + byteIndex] = (byte)(valueToWrite % 256);
valueToWrite -= buf[offset + byteIndex];
valueToWrite /= 256;
}
if (value < 0)
buf[offset + 7] |= 0x80;
}
const long c_fileSignature = 0x3034464649445342L;
const int c_headerSize = 32;
}
/// <summary>
/// A <see cref="Stream"/> that wraps another stream. One major feature of <see cref="WrappingStream"/> is that it does not dispose the
/// underlying stream when it is disposed if Ownership.None is used; this is useful when using classes such as <see cref="BinaryReader"/> and
/// <see cref="System.Security.Cryptography.CryptoStream"/> that take ownership of the stream passed to their constructors.
/// </summary>
/// <remarks>See <a href="http://code.logos.com/blog/2009/05/wrappingstream_implementation.html">WrappingStream Implementation</a>.</remarks>
public class WrappingStream : Stream
{
/// <summary>
/// Initializes a new instance of the <see cref="WrappingStream"/> class.
/// </summary>
/// <param name="streamBase">The wrapped stream.</param>
/// <param name="ownership">Use Owns if the wrapped stream should be disposed when this stream is disposed.</param>
public WrappingStream(Stream streamBase, Ownership ownership)
{
// check parameters
if (streamBase == null)
throw new ArgumentNullException("streamBase");
m_streamBase = streamBase;
m_ownership = ownership;
}
/// <summary>
/// Gets a value indicating whether the current stream supports reading.
/// </summary>
/// <returns><c>true</c> if the stream supports reading; otherwise, <c>false</c>.</returns>
public override bool CanRead
{
get { return m_streamBase == null ? false : m_streamBase.CanRead; }
}
/// <summary>
/// Gets a value indicating whether the current stream supports seeking.
/// </summary>
/// <returns><c>true</c> if the stream supports seeking; otherwise, <c>false</c>.</returns>
public override bool CanSeek
{
get { return m_streamBase == null ? false : m_streamBase.CanSeek; }
}
/// <summary>
/// Gets a value indicating whether the current stream supports writing.
/// </summary>
/// <returns><c>true</c> if the stream supports writing; otherwise, <c>false</c>.</returns>
public override bool CanWrite
{
get { return m_streamBase == null ? false : m_streamBase.CanWrite; }
}
/// <summary>
/// Gets the length in bytes of the stream.
/// </summary>
public override long Length
{
get { ThrowIfDisposed(); return m_streamBase.Length; }
}
/// <summary>
/// Gets or sets the position within the current stream.
/// </summary>
public override long Position
{
get { ThrowIfDisposed(); return m_streamBase.Position; }
set { ThrowIfDisposed(); m_streamBase.Position = value; }
}
/// <summary>
/// Begins an asynchronous read operation.
/// </summary>
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
ThrowIfDisposed();
return m_streamBase.BeginRead(buffer, offset, count, callback, state);
}
/// <summary>
/// Begins an asynchronous write operation.
/// </summary>
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
ThrowIfDisposed();
return m_streamBase.BeginWrite(buffer, offset, count, callback, state);
}
/// <summary>
/// Waits for the pending asynchronous read to complete.
/// </summary>
public override int EndRead(IAsyncResult asyncResult)
{
ThrowIfDisposed();
return m_streamBase.EndRead(asyncResult);
}
/// <summary>
/// Ends an asynchronous write operation.
/// </summary>
public override void EndWrite(IAsyncResult asyncResult)
{
ThrowIfDisposed();
m_streamBase.EndWrite(asyncResult);
}
/// <summary>
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
/// </summary>
public override void Flush()
{
ThrowIfDisposed();
m_streamBase.Flush();
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position
/// within the stream by the number of bytes read.
/// </summary>
public override int Read(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
return m_streamBase.Read(buffer, offset, count);
}
/// <summary>
/// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.
/// </summary>
public override int ReadByte()
{
ThrowIfDisposed();
return m_streamBase.ReadByte();
}
/// <summary>
/// Sets the position within the current stream.
/// </summary>
/// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the current stream.</returns>
public override long Seek(long offset, SeekOrigin origin)
{
ThrowIfDisposed();
return m_streamBase.Seek(offset, origin);
}
/// <summary>
/// Sets the length of the current stream.
/// </summary>
/// <param name="value">The desired length of the current stream in bytes.</param>
public override void SetLength(long value)
{
ThrowIfDisposed();
m_streamBase.SetLength(value);
}
/// <summary>
/// Writes a sequence of bytes to the current stream and advances the current position
/// within this stream by the number of bytes written.
/// </summary>
public override void Write(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
m_streamBase.Write(buffer, offset, count);
}
/// <summary>
/// Writes a byte to the current position in the stream and advances the position within the stream by one byte.
/// </summary>
public override void WriteByte(byte value)
{
ThrowIfDisposed();
m_streamBase.WriteByte(value);
}
/// <summary>
/// Gets the wrapped stream.
/// </summary>
/// <value>The wrapped stream.</value>
protected Stream WrappedStream
{
get { return m_streamBase; }
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="WrappingStream"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
{
try
{
// doesn't close the base stream, but just prevents access to it through this WrappingStream
if (disposing)
{
if (m_streamBase != null && m_ownership == Ownership.Owns)
m_streamBase.Dispose();
m_streamBase = null;
}
}
finally
{
base.Dispose(disposing);
}
}
private void ThrowIfDisposed()
{
// throws an ObjectDisposedException if this object has been disposed
if (m_streamBase == null)
throw new ObjectDisposedException(GetType().Name);
}
Stream m_streamBase;
readonly Ownership m_ownership;
}
/// <summary>
/// Indicates whether an object takes ownership of an item.
/// </summary>
public enum Ownership
{
/// <summary>
/// The object does not own this item.
/// </summary>
None,
/// <summary>
/// The object owns this item, and is responsible for releasing it.
/// </summary>
Owns
}
/// <summary>
/// Provides helper methods for working with <see cref="Stream"/>.
/// </summary>
public static class StreamUtility
{
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="count">The count of bytes to read.</param>
/// <returns>A new byte array containing the data read from the stream.</returns>
public static byte[] ReadExactly(this Stream stream, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException("count");
byte[] buffer = new byte[count];
ReadExactly(stream, buffer, 0, count);
return buffer;
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/> into
/// <paramref name="buffer"/>, starting at the byte given by <paramref name="offset"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The offset within the buffer at which data is first written.</param>
/// <param name="count">The count of bytes to read.</param>
public static void ReadExactly(this Stream stream, byte[] buffer, int offset, int count)
{
// check arguments
if (stream == null)
throw new ArgumentNullException("stream");
if (buffer == null)
throw new ArgumentNullException("buffer");
if (offset < 0 || offset > buffer.Length)
throw new ArgumentOutOfRangeException("offset");
if (count < 0 || buffer.Length - offset < count)
throw new ArgumentOutOfRangeException("count");
while (count > 0)
{
// read data
int bytesRead = stream.Read(buffer, offset, count);
// check for failure to read
if (bytesRead == 0)
throw new EndOfStreamException();
// move to next block
offset += bytesRead;
count -= bytesRead;
}
}
}
}

40
src/ContentType.cs Normal file
View File

@@ -0,0 +1,40 @@
using System;
using System.Linq;
using System.Xml;
namespace Squirrel.Core
{
internal static class ContentType
{
public static void Merge(XmlDocument doc)
{
var elements = new [] {
Tuple.Create("Default", "diff", "application/octet" ),
Tuple.Create("Default", "exe", "application/octet" ),
Tuple.Create("Default", "dll", "application/octet" ),
Tuple.Create("Default", "shasum", "text/plain" ),
};
var typesElement = doc.FirstChild.NextSibling;
if (typesElement.Name.ToLowerInvariant() != "types") {
throw new Exception("Invalid ContentTypes file, expected root node should be 'Types'");
}
var existingTypes = typesElement.ChildNodes.OfType<XmlElement>()
.Select(k => Tuple.Create(k.Name,
k.GetAttribute("Extension").ToLowerInvariant(),
k.GetAttribute("ContentType").ToLowerInvariant()));
elements
.Where(x => existingTypes.All(t => t.Item2 != x.Item2.ToLowerInvariant()))
.Select(element => {
var ret = doc.CreateElement(element.Item1, typesElement.NamespaceURI);
var ext = doc.CreateAttribute("Extension"); ext.Value = element.Item2;
var ct = doc.CreateAttribute("ContentType"); ct.Value = element.Item3;
new[] { ext, ct }.ForEach(x => ret.Attributes.Append(x));
return ret;
}).ForEach(x => typesElement.AppendChild(x));
}
}
}

263
src/DeltaPackage.cs Normal file
View File

@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Ionic.Zip;
using ReactiveUIMicro;
namespace Squirrel.Core
{
public interface IDeltaPackageBuilder
{
ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile);
ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile);
}
public class DeltaPackageBuilder : IEnableLogger, IDeltaPackageBuilder
{
public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile)
{
Contract.Requires(basePackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
if (basePackage.Version > newPackage.Version) {
var message = String.Format(
"You cannot create a delta package based on version {0} as it is a later version than {1}",
basePackage.Version,
newPackage.Version);
throw new InvalidOperationException(message);
}
if (basePackage.ReleasePackageFile == null) {
throw new ArgumentException("The base package's release file is null", "basePackage");
}
if (!File.Exists(basePackage.ReleasePackageFile)) {
throw new FileNotFoundException("The base package release does not exist", basePackage.ReleasePackageFile);
}
if (!File.Exists(newPackage.ReleasePackageFile)) {
throw new FileNotFoundException("The new package release does not exist", newPackage.ReleasePackageFile);
}
string baseTempPath = null;
string tempPath = null;
using (Utility.WithTempDirectory(out baseTempPath))
using (Utility.WithTempDirectory(out tempPath)) {
var baseTempInfo = new DirectoryInfo(baseTempPath);
var tempInfo = new DirectoryInfo(tempPath);
using (var zf = new ZipFile(basePackage.ReleasePackageFile)) {
zf.ExtractAll(baseTempInfo.FullName);
}
using (var zf = new ZipFile(newPackage.ReleasePackageFile)) {
zf.ExtractAll(tempInfo.FullName);
}
// Collect a list of relative paths under 'lib' and map them
// to their full name. We'll use this later to determine in
// the new version of the package whether the file exists or
// not.
var baseLibFiles = baseTempInfo.GetAllFilesRecursively()
.Where(x => x.FullName.ToLowerInvariant().Contains("lib" + Path.DirectorySeparatorChar))
.ToDictionary(k => k.FullName.Replace(baseTempInfo.FullName, ""), v => v.FullName);
var newLibDir = tempInfo.GetDirectories().First(x => x.Name.ToLowerInvariant() == "lib");
newLibDir.GetAllFilesRecursively()
.ForEach(libFile => createDeltaForSingleFile(libFile, tempInfo, baseLibFiles));
ReleasePackage.addDeltaFilesToContentTypes(tempInfo.FullName);
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(tempInfo.FullName);
zf.Save();
}
}
return new ReleasePackage(outputFile);
}
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
{
Contract.Requires(deltaPackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
string workingPath;
string deltaPath;
using (Utility.WithTempDirectory(out deltaPath))
using (Utility.WithTempDirectory(out workingPath))
using (var deltaZip = new ZipFile(deltaPackage.InputPackageFile))
using (var baseZip = new ZipFile(basePackage.InputPackageFile)) {
deltaZip.ExtractAll(deltaPath);
baseZip.ExtractAll(workingPath);
var pathsVisited = new List<string>();
var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(deltaPath + Path.DirectorySeparatorChar, ""))
.ToArray();
// Apply all of the .diff files
deltaPathRelativePaths
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(file => {
pathsVisited.Add(Regex.Replace(file, @".diff$", "").ToLowerInvariant());
applyDiffToFile(deltaPath, file, workingPath);
});
// Delete all of the files that were in the old package but
// not in the new one.
new DirectoryInfo(workingPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
.ForEach(x => {
this.Log().Info("{0} was in old package but not in new one, deleting", x);
File.Delete(Path.Combine(workingPath, x));
});
// Update all the files that aren't in 'lib' with the delta
// package's versions (i.e. the nuspec file, etc etc).
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(x => {
this.Log().Info("Updating metadata file: {0}", x);
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
});
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(workingPath);
zf.Save();
}
}
return new ReleasePackage(outputFile);
}
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary<string, string> baseFileListing)
{
// NB: There are three cases here that we'll handle:
//
// 1. Exists only in new => leave it alone, we'll use it directly.
// 2. Exists in both old and new => write a dummy file so we know
// to keep it.
// 3. Exists in old but changed in new => create a delta file
//
// The fourth case of "Exists only in old => delete it in new"
// is handled when we apply the delta package
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
if (!baseFileListing.ContainsKey(relativePath)) {
this.Log().Info("{0} not found in base package, marking as new", relativePath);
return;
}
var oldData = File.ReadAllBytes(baseFileListing[relativePath]);
var newData = File.ReadAllBytes(targetFile.FullName);
if (bytesAreIdentical(oldData, newData)) {
this.Log().Info("{0} hasn't changed, writing dummy file", relativePath);
File.Create(targetFile.FullName + ".diff").Dispose();
File.Create(targetFile.FullName + ".shasum").Dispose();
targetFile.Delete();
return;
}
this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName);
using (var of = File.Create(targetFile.FullName + ".diff")) {
BinaryPatchUtility.Create(oldData, newData, of);
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
targetFile.Delete();
}
}
void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory)
{
var inputFile = Path.Combine(deltaPath, relativeFilePath);
var finalTarget = Path.Combine(workingDirectory, Regex.Replace(relativeFilePath, @".diff$", ""));
var tempTargetFile = Path.GetTempFileName();
// NB: Zero-length diffs indicate the file hasn't actually changed
if (new FileInfo(inputFile).Length == 0) {
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
return;
}
if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(finalTarget)) {
this.Log().Info("Applying Diff to {0}", relativeFilePath);
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
}
try {
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
} catch (Exception) {
File.Delete(tempTargetFile);
throw;
}
} else {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(inputFile)) {
this.Log().Info("Adding new file: {0}", relativeFilePath);
inf.CopyTo(of);
}
}
if (File.Exists(finalTarget)) File.Delete(finalTarget);
var targetPath = Directory.GetParent(finalTarget);
if (!targetPath.Exists) targetPath.Create();
File.Move(tempTargetFile, finalTarget);
}
void verifyPatchedFile(string relativeFilePath, string inputFile, string tempTargetFile)
{
var shaFile = Regex.Replace(inputFile, @"\.diff$", ".shasum");
var expectedReleaseEntry = ReleaseEntry.ParseReleaseEntry(File.ReadAllText(shaFile, Encoding.UTF8));
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
throw new ChecksumFailedException() {Filename = relativeFilePath};
}
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
throw new ChecksumFailedException() {Filename = relativeFilePath};
}
}
bool bytesAreIdentical(byte[] oldData, byte[] newData)
{
if (oldData == null || newData == null) {
return oldData == newData;
}
if (oldData.LongLength != newData.LongLength) {
return false;
}
for(long i = 0; i < newData.LongLength; i++) {
if (oldData[i] != newData[i]) {
return false;
}
}
return true;
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Squirrel")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Squirrel")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3c25a7f9-3e99-4556-aba3-f820c74bb4da")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

200
src/ReleaseEntry.cs Normal file
View File

@@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NuGet;
using ReactiveUIMicro;
using Squirrel.Core.Extensions;
using System.Runtime.Serialization;
namespace Squirrel.Core
{
public interface IReleaseEntry
{
string SHA1 { get; }
string Filename { get; }
long Filesize { get; }
bool IsDelta { get; }
string EntryAsString { get; }
Version Version { get; }
string PackageName { get; }
string GetReleaseNotes(string packageDirectory);
}
[DataContract]
public class ReleaseEntry : IEnableLogger, IReleaseEntry
{
[DataMember] public string SHA1 { get; protected set; }
[DataMember] public string Filename { get; protected set; }
[DataMember] public long Filesize { get; protected set; }
[DataMember] public bool IsDelta { get; protected set; }
protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta)
{
Contract.Requires(sha1 != null && sha1.Length == 40);
Contract.Requires(filename != null);
Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false);
Contract.Requires(filesize > 0);
SHA1 = sha1; Filename = filename; Filesize = filesize; IsDelta = isDelta;
}
[IgnoreDataMember]
public string EntryAsString {
get { return String.Format("{0} {1} {2}", SHA1, Filename, Filesize); }
}
[IgnoreDataMember]
public Version Version { get { return Filename.ToVersion(); } }
[IgnoreDataMember]
public string PackageName {
get {
return Filename.Substring(0, Filename.IndexOfAny(new[] { '-', '.' }));
}
}
public string GetReleaseNotes(string packageDirectory)
{
var zp = new ZipPackage(Path.Combine(packageDirectory, Filename));
var t = zp.Id;
if (String.IsNullOrWhiteSpace(zp.ReleaseNotes))
throw new Exception(String.Format("Invalid 'ReleaseNotes' value in nuspec file at '{0}'", Path.Combine(packageDirectory, Filename)));
return zp.ReleaseNotes;
}
static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$");
static readonly Regex commentRegex = new Regex(@"#.*$");
public static ReleaseEntry ParseReleaseEntry(string entry)
{
Contract.Requires(entry != null);
entry = commentRegex.Replace(entry, "");
if (String.IsNullOrWhiteSpace(entry)) {
return null;
}
var m = entryRegex.Match(entry);
if (!m.Success) {
throw new Exception("Invalid release entry: " + entry);
}
if (m.Groups.Count != 4) {
throw new Exception("Invalid release entry: " + entry);
}
long size = Int64.Parse(m.Groups[3].Value);
bool isDelta = filenameIsDeltaFile(m.Groups[2].Value);
return new ReleaseEntry(m.Groups[1].Value, m.Groups[2].Value, size, isDelta);
}
public static IEnumerable<ReleaseEntry> ParseReleaseFile(string fileContents)
{
if (String.IsNullOrEmpty(fileContents)) {
return new ReleaseEntry[0];
}
var ret = fileContents.Split('\n')
.Where(x => !String.IsNullOrWhiteSpace(x))
.Select(ParseReleaseEntry)
.Where(x => x != null)
.ToArray();
return ret.Any(x => x == null) ? null : ret;
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, Stream stream)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(stream != null);
using (var sw = new StreamWriter(stream, Encoding.UTF8)) {
sw.Write(String.Join("\n", releaseEntries
.OrderBy(x => x.Version)
.ThenByDescending(x => x.IsDelta)
.Select(x => x.EntryAsString)));
}
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, string path)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(!String.IsNullOrEmpty(path));
using (var f = File.OpenWrite(path)) {
WriteReleaseFile(releaseEntries, f);
}
}
public static ReleaseEntry GenerateFromFile(Stream file, string filename)
{
Contract.Requires(file != null && file.CanRead);
Contract.Requires(!String.IsNullOrEmpty(filename));
var hash = Utility.CalculateStreamSHA1(file);
return new ReleaseEntry(hash, filename, file.Length, filenameIsDeltaFile(filename));
}
public static ReleaseEntry GenerateFromFile(string path)
{
using (var inf = File.OpenRead(path)) {
return GenerateFromFile(inf, Path.GetFileName(path));
}
}
public static void BuildReleasesFile(string releasePackagesDir, IFileSystemFactory fileSystemFactory = null)
{
fileSystemFactory = fileSystemFactory ?? AnonFileSystem.Default;
var packagesDir = fileSystemFactory.GetDirectoryInfo(releasePackagesDir);
// Generate release entries for all of the local packages
var entries = packagesDir.GetFiles("*.nupkg").MapReduce(x => Observable.Start(() => {
using (var file = x.OpenRead()) {
return GenerateFromFile(file, x.Name);
}
}, RxApp.TaskpoolScheduler)).First();
// Write the new RELEASES file to a temp file then move it into
// place
var tempFile = fileSystemFactory.CreateTempFile();
try {
if (entries.Count > 0) WriteReleaseFile(entries, tempFile.Item2);
} finally {
tempFile.Item2.Dispose();
}
var target = Path.Combine(packagesDir.FullName, "RELEASES");
if (File.Exists(target)) {
File.Delete(target);
}
fileSystemFactory.GetFileInfo(tempFile.Item1).MoveTo(target);
}
static bool filenameIsDeltaFile(string filename)
{
return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase);
}
public static ReleasePackage GetPreviousRelease(IEnumerable<ReleaseEntry> releaseEntries, IReleasePackage package, string targetDir)
{
if (releaseEntries == null || !releaseEntries.Any())
return null;
return releaseEntries
.Where(x => x.IsDelta == false)
.Where(x => x.Version < package.ToVersion())
.OrderByDescending(x => x.Version)
.Select(x => new ReleasePackage(Path.Combine(targetDir, x.Filename), true))
.FirstOrDefault();
}
}
}

281
src/ReleasePackage.cs Normal file
View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Ionic.Zip;
using NuGet;
using ReactiveUIMicro;
using Squirrel.Core.Extensions;
namespace Squirrel.Core
{
internal static class FrameworkTargetVersion
{
public static FrameworkName Net40 = new FrameworkName(".NETFramework,Version=v4.0");
public static FrameworkName Net45 = new FrameworkName(".NETFramework,Version=v4.5");
}
public interface IReleasePackage
{
string InputPackageFile { get; }
string ReleasePackageFile { get; }
string SuggestedReleaseFileName { get; }
string CreateReleasePackage(string outputFile, string packagesRootDir = null, Func<string, string> releaseNotesProcessor = null);
}
public static class VersionComparer
{
public static bool Matches(IVersionSpec versionSpec, SemanticVersion version)
{
if (versionSpec == null)
return true; // I CAN'T DEAL WITH THIS
bool minVersion;
if (versionSpec.MinVersion == null) {
minVersion = true; // no preconditon? LET'S DO IT
} else if (versionSpec.IsMinInclusive) {
minVersion = version >= versionSpec.MinVersion;
} else {
minVersion = version > versionSpec.MinVersion;
}
bool maxVersion;
if (versionSpec.MaxVersion == null) {
maxVersion = true; // no preconditon? LET'S DO IT
} else if (versionSpec.IsMaxInclusive) {
maxVersion = version <= versionSpec.MaxVersion;
} else {
maxVersion = version < versionSpec.MaxVersion;
}
return maxVersion && minVersion;
}
}
public class ReleasePackage : IEnableLogger, IReleasePackage
{
IEnumerable<IPackage> localPackageCache;
public ReleasePackage(string inputPackageFile, bool isReleasePackage = false)
{
InputPackageFile = inputPackageFile;
if (isReleasePackage) {
ReleasePackageFile = inputPackageFile;
}
}
public string InputPackageFile { get; protected set; }
public string ReleasePackageFile { get; protected set; }
public string SuggestedReleaseFileName {
get {
var zp = new ZipPackage(InputPackageFile);
return String.Format("{0}-{1}-full.nupkg", zp.Id, zp.Version);
}
}
public Version Version { get { return InputPackageFile.ToVersion(); } }
public string CreateReleasePackage(string outputFile, string packagesRootDir = null, Func<string, string> releaseNotesProcessor = null)
{
Contract.Requires(!String.IsNullOrEmpty(outputFile));
if (ReleasePackageFile != null) {
return ReleasePackageFile;
}
var package = new ZipPackage(InputPackageFile);
// we can tell from here what platform(s) the package targets
// but given this is a simple package we only
// ever expect one entry here (crash hard otherwise)
var frameworks = package.GetSupportedFrameworks();
if (frameworks.Count() > 1) {
var platforms = frameworks
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
throw new InvalidOperationException(String.Format(
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
}
var targetFramework = frameworks.Single();
// Recursively walk the dependency tree and extract all of the
// dependent packages into the a temporary directory
var dependencies = findAllDependentPackages(
package,
new LocalPackageRepository(packagesRootDir),
frameworkName: targetFramework);
string tempPath = null;
using (Utility.WithTempDirectory(out tempPath)) {
var tempDir = new DirectoryInfo(tempPath);
using(var zf = new ZipFile(InputPackageFile)) {
zf.ExtractAll(tempPath);
}
extractDependentPackages(dependencies, tempDir, targetFramework);
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
removeDependenciesFromPackageSpec(specPath);
removeDeveloperDocumentation(tempDir);
if (releaseNotesProcessor != null) {
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
}
addDeltaFilesToContentTypes(tempDir.FullName);
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(tempPath);
zf.Save();
}
ReleasePackageFile = outputFile;
return ReleasePackageFile;
}
}
void extractDependentPackages(IEnumerable<IPackage> dependencies, DirectoryInfo tempPath, FrameworkName framework)
{
dependencies.ForEach(pkg => {
this.Log().Info("Scanning {0}", pkg.Id);
pkg.GetLibFiles().ForEach(file => {
var outPath = new FileInfo(Path.Combine(tempPath.FullName, file.Path));
if (!VersionUtility.IsCompatible(framework , new[] { file.TargetFramework }))
{
this.Log().Info("Ignoring {0} as the target framework is not compatible", outPath);
return;
}
Directory.CreateDirectory(outPath.Directory.FullName);
using (var of = File.Create(outPath.FullName)) {
this.Log().Info("Writing {0} to {1}", file.Path, outPath);
file.GetStream().CopyTo(of);
}
});
});
}
void removeDeveloperDocumentation(DirectoryInfo expandedRepoPath)
{
expandedRepoPath.GetAllFilesRecursively()
.Where(x => x.Name.EndsWith(".dll", true, CultureInfo.InvariantCulture))
.Select(x => new FileInfo(x.FullName.ToLowerInvariant().Replace(".dll", ".xml")))
.Where(x => x.Exists)
.ForEach(x => x.Delete());
}
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
{
var doc = new XmlDocument();
doc.Load(specPath);
// XXX: This code looks full tart
var metadata = doc.DocumentElement.ChildNodes
.OfType<XmlElement>()
.First(x => x.Name.ToLowerInvariant() == "metadata");
var releaseNotes = metadata.ChildNodes
.OfType<XmlElement>()
.FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes");
if (releaseNotes == null) {
this.Log().Info("No release notes found in {0}", specPath);
return;
}
releaseNotes.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
releaseNotesProcessor(releaseNotes.InnerText));
doc.Save(specPath);
}
void removeDependenciesFromPackageSpec(string specPath)
{
var xdoc = new XmlDocument();
xdoc.Load(specPath);
var metadata = xdoc.DocumentElement.FirstChild;
var dependenciesNode = metadata.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.Name.ToLowerInvariant() == "dependencies");
if (dependenciesNode != null) {
metadata.RemoveChild(dependenciesNode);
}
xdoc.Save(specPath);
}
IEnumerable<IPackage> findAllDependentPackages(
IPackage package = null,
IPackageRepository packageRepository = null,
HashSet<string> packageCache = null,
FrameworkName frameworkName = null)
{
package = package ?? new ZipPackage(InputPackageFile);
packageCache = packageCache ?? new HashSet<string>();
var deps = package.DependencySets
.Where(x => x.TargetFramework == null
|| x.TargetFramework == frameworkName)
.SelectMany(x => x.Dependencies);
return deps.SelectMany(dependency => {
var ret = matchPackage(packageRepository, dependency.Id, dependency.VersionSpec);
if (ret == null) {
var message = String.Format("Couldn't find file for package in {1}: {0}", dependency.Id, packageRepository.Source);
this.Log().Error(message);
throw new Exception(message);
}
if (packageCache.Contains(ret.GetFullName())) {
return Enumerable.Empty<IPackage>();
}
packageCache.Add(ret.GetFullName());
return findAllDependentPackages(ret, packageRepository, packageCache, frameworkName).StartWith(ret).Distinct(y => y.GetFullName());
}).ToArray();
}
IPackage matchPackage(IPackageRepository packageRepository, string id, IVersionSpec version)
{
return packageRepository.FindPackagesById(id).FirstOrDefault(x => VersionComparer.Matches(version, x.Version));
}
static internal void addDeltaFilesToContentTypes(string rootDirectory)
{
var doc = new XmlDocument();
var path = Path.Combine(rootDirectory, "[Content_Types].xml");
doc.Load(path);
ContentType.Merge(doc);
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
doc.Save(sw);
}
}
}
public class ChecksumFailedException : Exception
{
public string Filename { get; set; }
}
}

53
src/Squirrel.csproj Normal file
View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Squirrel</RootNamespace>
<AssemblyName>Squirrel</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Class1.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

334
src/Utility.cs Normal file
View File

@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Threading;
using ReactiveUIMicro;
using System.Text;
namespace Squirrel.Core
{
public static class Utility
{
public static IEnumerable<FileInfo> GetAllFilesRecursively(this DirectoryInfo rootPath)
{
Contract.Requires(rootPath != null);
return rootPath.GetDirectories()
.SelectMany(GetAllFilesRecursively)
.Concat(rootPath.GetFiles());
}
public static IEnumerable<string> GetAllFilePathsRecursively(string rootPath)
{
Contract.Requires(rootPath != null);
return Directory.GetDirectories(rootPath)
.SelectMany(GetAllFilePathsRecursively)
.Concat(Directory.GetFiles(rootPath));
}
public static string CalculateFileSHA1(string filePath)
{
Contract.Requires(filePath != null);
using (var stream = File.OpenRead(filePath)) {
return CalculateStreamSHA1(stream);
}
}
public static string CalculateStreamSHA1(Stream file)
{
Contract.Requires(file != null && file.CanRead);
using (var sha1 = SHA1.Create()) {
return BitConverter.ToString(sha1.ComputeHash(file)).Replace("-", String.Empty);
}
}
public static IObservable<Unit> CopyToAsync(string from, string to)
{
Contract.Requires(!String.IsNullOrEmpty(from) && File.Exists(from));
Contract.Requires(!String.IsNullOrEmpty(to));
if (!File.Exists(from)) {
Log().Warn("The file {0} does not exist", from);
// TODO: should we fail this operation?
return Observable.Return(Unit.Default);
}
// XXX: SafeCopy
return Observable.Start(() => File.Copy(from, to, true), RxApp.TaskpoolScheduler);
}
public static void Retry(this Action block, int retries = 2)
{
Contract.Requires(retries > 0);
Func<object> thunk = () => {
block();
return null;
};
thunk.Retry(retries);
}
public static T Retry<T>(this Func<T> block, int retries = 2)
{
Contract.Requires(retries > 0);
while (true) {
try {
T ret = block();
return ret;
} catch (Exception) {
if (retries == 0) {
throw;
}
retries--;
Thread.Sleep(250);
}
}
}
public static IObservable<IList<TRet>> MapReduce<T, TRet>(this IObservable<T> This, Func<T, IObservable<TRet>> selector, int degreeOfParallelism = 4)
{
return This.Select(x => Observable.Defer(() => selector(x))).Merge(degreeOfParallelism).ToList();
}
public static IObservable<IList<TRet>> MapReduce<T, TRet>(this IEnumerable<T> This, Func<T, IObservable<TRet>> selector, int degreeOfParallelism = 4)
{
return This.ToObservable().Select(x => Observable.Defer(() => selector(x))).Merge(degreeOfParallelism).ToList();
}
static string directoryChars;
public static IDisposable WithTempDirectory(out string path)
{
var di = new DirectoryInfo(Environment.GetEnvironmentVariable("SQUIRREL_TEMP") ?? Environment.GetEnvironmentVariable("TEMP") ?? "");
if (!di.Exists) {
throw new Exception("%TEMP% isn't defined, go set it");
}
var tempDir = default(DirectoryInfo);
directoryChars = directoryChars ?? (
"abcdefghijklmnopqrstuvwxyz" +
Enumerable.Range(0x4E00, 0x9FCC - 0x4E00) // CJK UNIFIED IDEOGRAPHS
.Aggregate(new StringBuilder(), (acc, x) => { acc.Append(Char.ConvertFromUtf32(x)); return acc; })
.ToString());
foreach (var c in directoryChars) {
var target = Path.Combine(di.FullName, c.ToString());
if (!File.Exists(target) && !Directory.Exists(target)) {
Directory.CreateDirectory(target);
tempDir = new DirectoryInfo(target);
break;
}
}
path = tempDir.FullName;
return Disposable.Create(() =>
DeleteDirectory(tempDir.FullName).Wait());
}
public static IObservable<Unit> DeleteDirectory(string directoryPath, IScheduler scheduler = null)
{
Contract.Requires(!String.IsNullOrEmpty(directoryPath));
scheduler = scheduler ?? RxApp.TaskpoolScheduler;
Log().Info("Starting to delete folder: {0}", directoryPath);
if (!Directory.Exists(directoryPath)) {
Log().Warn("DeleteDirectory: does not exist - {0}", directoryPath);
return Observable.Return(Unit.Default);
}
// From http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502
var files = new string[0];
try {
files = Directory.GetFiles(directoryPath);
} catch (UnauthorizedAccessException ex) {
var message = String.Format("The files inside {0} could not be read", directoryPath);
Log().Warn(message, ex);
}
var dirs = new string[0];
try {
dirs = Directory.GetDirectories(directoryPath);
} catch (UnauthorizedAccessException ex) {
var message = String.Format("The directories inside {0} could not be read", directoryPath);
Log().Warn(message, ex);
}
var fileOperations = files.MapReduce(file =>
Observable.Start(() => {
Log().Debug("Now deleting file: {0}", file);
File.SetAttributes(file, FileAttributes.Normal);
File.Delete(file);
}, scheduler))
.Select(_ => Unit.Default);
var directoryOperations =
dirs.MapReduce(dir => DeleteDirectory(dir, scheduler)
.Retry(3))
.Select(_ => Unit.Default);
return fileOperations
.Merge(directoryOperations, scheduler)
.ToList() // still feeling a bit icky
.Select(_ => {
Log().Debug("Now deleting folder: {0}", directoryPath);
File.SetAttributes(directoryPath, FileAttributes.Normal);
try {
Directory.Delete(directoryPath, false);
} catch (Exception ex) {
var message = String.Format("DeleteDirectory: could not delete - {0}", directoryPath);
Log().ErrorException(message, ex);
}
return Unit.Default;
});
}
public static Tuple<string, Stream> CreateTempFile()
{
var path = Path.GetTempFileName();
return Tuple.Create(path, (Stream) File.OpenWrite(path));
}
static TAcc scan<T, TAcc>(this IEnumerable<T> This, TAcc initialValue, Func<TAcc, T, TAcc> accFunc)
{
TAcc acc = initialValue;
foreach (var x in This)
{
acc = accFunc(acc, x);
}
return acc;
}
public static void DeleteDirectoryAtNextReboot(string directoryPath)
{
var di = new DirectoryInfo(directoryPath);
if (!di.Exists) {
Log().Warn("DeleteDirectoryAtNextReboot: does not exist - {0}", directoryPath);
return;
}
// NB: MoveFileEx blows up if you're a non-admin, so you always need a backup plan
di.GetFiles().ForEach(x => safeDeleteFileAtNextReboot(x.FullName));
di.GetDirectories().ForEach(x => DeleteDirectoryAtNextReboot(x.FullName));
safeDeleteFileAtNextReboot(directoryPath);
}
static void safeDeleteFileAtNextReboot(string name)
{
if (MoveFileEx(name, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT)) return;
// thank you, http://www.pinvoke.net/default.aspx/coredll.getlasterror
var lastError = Marshal.GetLastWin32Error();
Log().Error("safeDeleteFileAtNextReboot: failed - {0} - {1}", name, lastError);
}
static IRxUIFullLogger Log()
{
return LogManager.GetLogger(typeof(Utility));
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
[Flags]
enum MoveFileFlags
{
MOVEFILE_REPLACE_EXISTING = 0x00000001,
MOVEFILE_COPY_ALLOWED = 0x00000002,
MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004,
MOVEFILE_WRITE_THROUGH = 0x00000008,
MOVEFILE_CREATE_HARDLINK = 0x00000010,
MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
}
}
public sealed class SingleGlobalInstance : IDisposable
{
readonly static object gate = 42;
bool HasHandle = false;
Mutex mutex;
EventLoopScheduler lockScheduler = new EventLoopScheduler();
public SingleGlobalInstance(string key, int timeOut)
{
if (RxApp.InUnitTestRunner()) {
HasHandle = Observable.Start(() => Monitor.TryEnter(gate, timeOut), lockScheduler).First();
if (HasHandle == false)
throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
return;
}
initMutex(key);
try
{
if (timeOut <= 0)
HasHandle = Observable.Start(() => mutex.WaitOne(Timeout.Infinite, false), lockScheduler).First();
else
HasHandle = Observable.Start(() => mutex.WaitOne(timeOut, false), lockScheduler).First();
if (HasHandle == false)
throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
}
catch (AbandonedMutexException)
{
HasHandle = true;
}
}
private void initMutex(string key)
{
string mutexId = string.Format("Global\\{{{0}}}", key);
mutex = new Mutex(false, mutexId);
var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
var securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryoneRule);
mutex.SetAccessControl(securitySettings);
}
public void Dispose()
{
if (HasHandle && RxApp.InUnitTestRunner()) {
Observable.Start(() => Monitor.Exit(gate), lockScheduler).First();
HasHandle = false;
}
if (HasHandle && mutex != null) {
Observable.Start(() => mutex.ReleaseMutex(), lockScheduler).First();
HasHandle = false;
}
lockScheduler.Dispose();
}
}
}

5
src/packages.config Normal file
View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.Xdt" version="2.1.1" targetFramework="net45" />
<package id="NuGet.Core" version="2.8.2" targetFramework="net45" />
</packages>