354a7ffa75
Former-commit-id: 9581b2879201a39de16b05882d3ad003b4a23a64
663 lines
18 KiB
C#
663 lines
18 KiB
C#
/* -*- Mode: csharp; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
|
|
//
|
|
// DeflateStream.cs
|
|
//
|
|
// Authors:
|
|
// Christopher James Lahey <clahey@ximian.com>
|
|
// Gonzalo Paniagua Javier (gonzalo@novell.com)
|
|
// Marek Safar (marek.safar@gmail.com)
|
|
//
|
|
// (c) Copyright 2004,2009 Novell, Inc. <http://www.novell.com>
|
|
// Copyright (C) 2013 Xamarin Inc (http://www.xamarin.com)
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining
|
|
// a copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to
|
|
// permit persons to whom the Software is furnished to do so, subject to
|
|
// the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be
|
|
// included in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
//
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using System.Runtime.Remoting.Messaging;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace System.IO.Compression
|
|
{
|
|
public class DeflateStream : Stream
|
|
{
|
|
delegate int ReadMethod (byte[] array, int offset, int count);
|
|
delegate void WriteMethod (byte[] array, int offset, int count);
|
|
|
|
Stream base_stream;
|
|
CompressionMode mode;
|
|
bool leaveOpen;
|
|
bool disposed;
|
|
DeflateStreamNative native;
|
|
|
|
public DeflateStream (Stream stream, CompressionMode mode) :
|
|
this (stream, mode, false, false)
|
|
{
|
|
}
|
|
|
|
public DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen) :
|
|
this (stream, mode, leaveOpen, false)
|
|
{
|
|
}
|
|
|
|
internal DeflateStream (Stream stream, CompressionMode mode, bool leaveOpen, int windowsBits) :
|
|
this (stream, mode, leaveOpen, true)
|
|
{
|
|
}
|
|
|
|
internal DeflateStream (Stream compressedStream, CompressionMode mode, bool leaveOpen, bool gzip)
|
|
{
|
|
if (compressedStream == null)
|
|
throw new ArgumentNullException ("compressedStream");
|
|
|
|
if (mode != CompressionMode.Compress && mode != CompressionMode.Decompress)
|
|
throw new ArgumentException ("mode");
|
|
|
|
this.base_stream = compressedStream;
|
|
|
|
this.native = DeflateStreamNative.Create (compressedStream, mode, gzip);
|
|
if (this.native == null) {
|
|
throw new NotImplementedException ("Failed to initialize zlib. You probably have an old zlib installed. Version 1.2.0.4 or later is required.");
|
|
}
|
|
this.mode = mode;
|
|
this.leaveOpen = leaveOpen;
|
|
}
|
|
|
|
public DeflateStream (Stream stream, CompressionLevel compressionLevel)
|
|
: this (stream, compressionLevel, false, false)
|
|
{
|
|
}
|
|
|
|
public DeflateStream (Stream stream, CompressionLevel compressionLevel, bool leaveOpen)
|
|
: this (stream, compressionLevel, leaveOpen, false)
|
|
{
|
|
}
|
|
|
|
internal DeflateStream (Stream stream, CompressionLevel compressionLevel, bool leaveOpen, int windowsBits)
|
|
: this (stream, compressionLevel, leaveOpen, true)
|
|
{
|
|
}
|
|
|
|
internal DeflateStream (Stream stream, CompressionLevel compressionLevel, bool leaveOpen, bool gzip)
|
|
: this (stream, CompressionMode.Compress, leaveOpen, gzip)
|
|
{
|
|
}
|
|
|
|
~DeflateStream () => Dispose (false);
|
|
|
|
protected override void Dispose (bool disposing)
|
|
{
|
|
if (disposing)
|
|
GC.SuppressFinalize (this);
|
|
|
|
native.Dispose (disposing);
|
|
|
|
if (disposing && !disposed) {
|
|
disposed = true;
|
|
|
|
if (!leaveOpen) {
|
|
Stream st = base_stream;
|
|
if (st != null)
|
|
st.Close ();
|
|
base_stream = null;
|
|
}
|
|
}
|
|
|
|
base.Dispose (disposing);
|
|
}
|
|
|
|
unsafe int ReadInternal (byte[] array, int offset, int count)
|
|
{
|
|
if (count == 0)
|
|
return 0;
|
|
|
|
fixed (byte *b = array) {
|
|
IntPtr ptr = new IntPtr (b + offset);
|
|
return native.ReadZStream (ptr, count);
|
|
}
|
|
}
|
|
|
|
internal ValueTask<int> ReadAsyncMemory (Memory<byte> destination, CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException ();
|
|
}
|
|
|
|
internal int ReadCore (Span<byte> destination)
|
|
{
|
|
throw new NotImplementedException ();
|
|
}
|
|
|
|
public override int Read (byte[] array, int offset, int count)
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (GetType ().FullName);
|
|
if (array == null)
|
|
throw new ArgumentNullException ("Destination array is null.");
|
|
if (!CanRead)
|
|
throw new InvalidOperationException ("Stream does not support reading.");
|
|
int len = array.Length;
|
|
if (offset < 0 || count < 0)
|
|
throw new ArgumentException ("Dest or count is negative.");
|
|
if (offset > len)
|
|
throw new ArgumentException ("destination offset is beyond array size");
|
|
if ((offset + count) > len)
|
|
throw new ArgumentException ("Reading would overrun buffer");
|
|
|
|
return ReadInternal (array, offset, count);
|
|
}
|
|
|
|
unsafe void WriteInternal (byte[] array, int offset, int count)
|
|
{
|
|
if (count == 0)
|
|
return;
|
|
|
|
fixed (byte *b = array) {
|
|
IntPtr ptr = new IntPtr (b + offset);
|
|
native.WriteZStream (ptr, count);
|
|
}
|
|
}
|
|
|
|
internal ValueTask WriteAsyncMemory (ReadOnlyMemory<byte> source, CancellationToken cancellationToken)
|
|
{
|
|
throw new NotImplementedException ();
|
|
}
|
|
|
|
internal void WriteCore (ReadOnlySpan<byte> source)
|
|
{
|
|
throw new NotImplementedException ();
|
|
}
|
|
|
|
public override void Write (byte[] array, int offset, int count)
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (GetType ().FullName);
|
|
|
|
if (array == null)
|
|
throw new ArgumentNullException ("array");
|
|
|
|
if (offset < 0)
|
|
throw new ArgumentOutOfRangeException ("offset");
|
|
|
|
if (count < 0)
|
|
throw new ArgumentOutOfRangeException ("count");
|
|
|
|
if (!CanWrite)
|
|
throw new NotSupportedException ("Stream does not support writing");
|
|
|
|
if (offset > array.Length - count)
|
|
throw new ArgumentException ("Buffer too small. count/offset wrong.");
|
|
|
|
WriteInternal (array, offset, count);
|
|
}
|
|
|
|
public override void Flush ()
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (GetType ().FullName);
|
|
|
|
if (CanWrite) {
|
|
native.Flush ();
|
|
}
|
|
}
|
|
|
|
public override IAsyncResult BeginRead (byte [] array, int offset, int count,
|
|
AsyncCallback asyncCallback, object asyncState)
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (GetType ().FullName);
|
|
|
|
if (!CanRead)
|
|
throw new NotSupportedException ("This stream does not support reading");
|
|
|
|
if (array == null)
|
|
throw new ArgumentNullException ("array");
|
|
|
|
if (count < 0)
|
|
throw new ArgumentOutOfRangeException ("count", "Must be >= 0");
|
|
|
|
if (offset < 0)
|
|
throw new ArgumentOutOfRangeException ("offset", "Must be >= 0");
|
|
|
|
if (count + offset > array.Length)
|
|
throw new ArgumentException ("Buffer too small. count/offset wrong.");
|
|
|
|
ReadMethod r = new ReadMethod (ReadInternal);
|
|
return r.BeginInvoke (array, offset, count, asyncCallback, asyncState);
|
|
}
|
|
|
|
public override IAsyncResult BeginWrite (byte [] array, int offset, int count,
|
|
AsyncCallback asyncCallback, object asyncState)
|
|
{
|
|
if (disposed)
|
|
throw new ObjectDisposedException (GetType ().FullName);
|
|
|
|
if (!CanWrite)
|
|
throw new InvalidOperationException ("This stream does not support writing");
|
|
|
|
if (array == null)
|
|
throw new ArgumentNullException ("array");
|
|
|
|
if (count < 0)
|
|
throw new ArgumentOutOfRangeException ("count", "Must be >= 0");
|
|
|
|
if (offset < 0)
|
|
throw new ArgumentOutOfRangeException ("offset", "Must be >= 0");
|
|
|
|
if (count + offset > array.Length)
|
|
throw new ArgumentException ("Buffer too small. count/offset wrong.");
|
|
|
|
WriteMethod w = new WriteMethod (WriteInternal);
|
|
return w.BeginInvoke (array, offset, count, asyncCallback, asyncState);
|
|
}
|
|
|
|
public override int EndRead(IAsyncResult asyncResult)
|
|
{
|
|
if (asyncResult == null)
|
|
throw new ArgumentNullException ("asyncResult");
|
|
|
|
AsyncResult ares = asyncResult as AsyncResult;
|
|
if (ares == null)
|
|
throw new ArgumentException ("Invalid IAsyncResult", "asyncResult");
|
|
|
|
ReadMethod r = ares.AsyncDelegate as ReadMethod;
|
|
if (r == null)
|
|
throw new ArgumentException ("Invalid IAsyncResult", "asyncResult");
|
|
|
|
return r.EndInvoke (asyncResult);
|
|
}
|
|
|
|
public override void EndWrite (IAsyncResult asyncResult)
|
|
{
|
|
if (asyncResult == null)
|
|
throw new ArgumentNullException ("asyncResult");
|
|
|
|
AsyncResult ares = asyncResult as AsyncResult;
|
|
if (ares == null)
|
|
throw new ArgumentException ("Invalid IAsyncResult", "asyncResult");
|
|
|
|
WriteMethod w = ares.AsyncDelegate as WriteMethod;
|
|
if (w == null)
|
|
throw new ArgumentException ("Invalid IAsyncResult", "asyncResult");
|
|
|
|
w.EndInvoke (asyncResult);
|
|
return;
|
|
}
|
|
|
|
public override long Seek (long offset, SeekOrigin origin)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public override void SetLength (long value)
|
|
{
|
|
throw new NotSupportedException();
|
|
}
|
|
|
|
public Stream BaseStream {
|
|
get { return base_stream; }
|
|
}
|
|
|
|
public override bool CanRead {
|
|
get { return !disposed && mode == CompressionMode.Decompress && base_stream.CanRead; }
|
|
}
|
|
|
|
public override bool CanSeek {
|
|
get { return false; }
|
|
}
|
|
|
|
public override bool CanWrite {
|
|
get { return !disposed && mode == CompressionMode.Compress && base_stream.CanWrite; }
|
|
}
|
|
|
|
public override long Length {
|
|
get { throw new NotSupportedException(); }
|
|
}
|
|
|
|
public override long Position {
|
|
get { throw new NotSupportedException(); }
|
|
set { throw new NotSupportedException(); }
|
|
}
|
|
}
|
|
|
|
class DeflateStreamNative
|
|
{
|
|
const int BufferSize = 4096;
|
|
|
|
[UnmanagedFunctionPointer (CallingConvention.Cdecl)]
|
|
delegate int UnmanagedReadOrWrite (IntPtr buffer, int length, IntPtr data);
|
|
|
|
UnmanagedReadOrWrite feeder; // This will be passed to unmanaged code and used there
|
|
|
|
Stream base_stream;
|
|
SafeDeflateStreamHandle z_stream;
|
|
GCHandle data;
|
|
bool disposed;
|
|
byte [] io_buffer;
|
|
Exception last_error;
|
|
|
|
private DeflateStreamNative ()
|
|
{
|
|
}
|
|
|
|
public static DeflateStreamNative Create (Stream compressedStream, CompressionMode mode, bool gzip)
|
|
{
|
|
var dsn = new DeflateStreamNative ();
|
|
dsn.data = GCHandle.Alloc (dsn);
|
|
dsn.feeder = mode == CompressionMode.Compress ? new UnmanagedReadOrWrite (UnmanagedWrite) : new UnmanagedReadOrWrite (UnmanagedRead);
|
|
dsn.z_stream = CreateZStream (mode, gzip, dsn.feeder, GCHandle.ToIntPtr (dsn.data));
|
|
if (dsn.z_stream.IsInvalid) {
|
|
dsn.Dispose (true);
|
|
return null;
|
|
}
|
|
|
|
dsn.base_stream = compressedStream;
|
|
return dsn;
|
|
}
|
|
|
|
~DeflateStreamNative ()
|
|
{
|
|
Dispose (false);
|
|
}
|
|
|
|
public void Dispose (bool disposing)
|
|
{
|
|
if (disposing && !disposed) {
|
|
disposed = true;
|
|
GC.SuppressFinalize (this);
|
|
} else {
|
|
// When we are in the finalizer we don't want to access the underlying stream anymore
|
|
base_stream = Stream.Null;
|
|
}
|
|
|
|
io_buffer = null;
|
|
|
|
if (z_stream != null) {
|
|
z_stream.Dispose();
|
|
}
|
|
|
|
if (data.IsAllocated) {
|
|
data.Free ();
|
|
}
|
|
}
|
|
|
|
public void Flush ()
|
|
{
|
|
var res = Flush (z_stream);
|
|
CheckResult (res, "Flush");
|
|
}
|
|
|
|
public int ReadZStream (IntPtr buffer, int length)
|
|
{
|
|
var res = ReadZStream (z_stream, buffer, length);
|
|
CheckResult (res, "ReadInternal");
|
|
return res;
|
|
}
|
|
|
|
public void WriteZStream (IntPtr buffer, int length)
|
|
{
|
|
var res = WriteZStream (z_stream, buffer, length);
|
|
CheckResult (res, "WriteInternal");
|
|
}
|
|
|
|
[Mono.Util.MonoPInvokeCallback (typeof (UnmanagedReadOrWrite))]
|
|
static int UnmanagedRead (IntPtr buffer, int length, IntPtr data)
|
|
{
|
|
GCHandle s = GCHandle.FromIntPtr (data);
|
|
var self = s.Target as DeflateStreamNative;
|
|
if (self == null)
|
|
return -1;
|
|
return self.UnmanagedRead (buffer, length);
|
|
}
|
|
|
|
int UnmanagedRead (IntPtr buffer, int length)
|
|
{
|
|
if (io_buffer == null)
|
|
io_buffer = new byte [BufferSize];
|
|
|
|
int count = Math.Min (length, io_buffer.Length);
|
|
int n;
|
|
|
|
try {
|
|
n = base_stream.Read (io_buffer, 0, count);
|
|
} catch (Exception ex) {
|
|
last_error = ex;
|
|
return -12;
|
|
}
|
|
|
|
if (n > 0)
|
|
Marshal.Copy (io_buffer, 0, buffer, n);
|
|
|
|
return n;
|
|
}
|
|
|
|
[Mono.Util.MonoPInvokeCallback (typeof (UnmanagedReadOrWrite))]
|
|
static int UnmanagedWrite (IntPtr buffer, int length, IntPtr data)
|
|
{
|
|
GCHandle s = GCHandle.FromIntPtr (data);
|
|
var self = s.Target as DeflateStreamNative;
|
|
if (self == null)
|
|
return -1;
|
|
return self.UnmanagedWrite (buffer, length);
|
|
}
|
|
|
|
|
|
int UnmanagedWrite (IntPtr buffer, int length)
|
|
{
|
|
int total = 0;
|
|
while (length > 0) {
|
|
if (io_buffer == null)
|
|
io_buffer = new byte [BufferSize];
|
|
|
|
int count = Math.Min (length, io_buffer.Length);
|
|
Marshal.Copy (buffer, io_buffer, 0, count);
|
|
try {
|
|
base_stream.Write (io_buffer, 0, count);
|
|
} catch (Exception ex) {
|
|
last_error = ex;
|
|
return -12;
|
|
}
|
|
unsafe {
|
|
buffer = new IntPtr ((byte *) buffer.ToPointer () + count);
|
|
}
|
|
length -= count;
|
|
total += count;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
void CheckResult (int result, string where)
|
|
{
|
|
if (result >= 0)
|
|
return;
|
|
|
|
var throw_me = Interlocked.Exchange (ref last_error, null);
|
|
if (throw_me != null)
|
|
throw throw_me;
|
|
|
|
string error;
|
|
switch (result) {
|
|
case -1: // Z_ERRNO
|
|
error = "Unknown error"; // Marshal.GetLastWin32() ?
|
|
break;
|
|
case -2: // Z_STREAM_ERROR
|
|
error = "Internal error";
|
|
break;
|
|
case -3: // Z_DATA_ERROR
|
|
error = "Corrupted data";
|
|
break;
|
|
case -4: // Z_MEM_ERROR
|
|
error = "Not enough memory";
|
|
break;
|
|
case -5: // Z_BUF_ERROR
|
|
error = "Internal error (no progress possible)";
|
|
break;
|
|
case -6: // Z_VERSION_ERROR
|
|
error = "Invalid version";
|
|
break;
|
|
case -10:
|
|
error = "Invalid argument(s)";
|
|
break;
|
|
case -11:
|
|
error = "IO error";
|
|
break;
|
|
default:
|
|
error = "Unknown error";
|
|
break;
|
|
}
|
|
|
|
throw new IOException (error + " " + where);
|
|
}
|
|
|
|
#if ORBIS
|
|
static SafeDeflateStreamHandle CreateZStream (CompressionMode compress, bool gzip, UnmanagedReadOrWrite feeder, IntPtr data)
|
|
{
|
|
throw new PlatformNotSupportedException ();
|
|
}
|
|
|
|
static int CloseZStream (IntPtr stream)
|
|
{
|
|
throw new PlatformNotSupportedException ();
|
|
}
|
|
|
|
static int Flush (SafeDeflateStreamHandle stream)
|
|
{
|
|
throw new PlatformNotSupportedException ();
|
|
}
|
|
|
|
static int ReadZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length)
|
|
{
|
|
throw new PlatformNotSupportedException ();
|
|
}
|
|
|
|
static int WriteZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length)
|
|
{
|
|
throw new PlatformNotSupportedException ();
|
|
}
|
|
#elif MONOTOUCH || MONODROID
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
static extern IntPtr CreateZStream (int compress, bool gzip, IntPtr feeder, IntPtr data);
|
|
|
|
static SafeDeflateStreamHandle CreateZStream (CompressionMode compress, bool gzip, UnmanagedReadOrWrite feeder, IntPtr data)
|
|
{
|
|
SafeDeflateStreamHandle res;
|
|
try {} finally {
|
|
res = new SafeDeflateStreamHandle (CreateZStream ((int) compress, gzip, Marshal.GetFunctionPointerForDelegate<UnmanagedReadOrWrite> (feeder), data));
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
static extern int CloseZStream (IntPtr stream);
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
static extern int Flush (IntPtr stream);
|
|
|
|
static int Flush (SafeDeflateStreamHandle stream)
|
|
{
|
|
bool release = false;
|
|
try {
|
|
stream.DangerousAddRef (ref release);
|
|
return Flush (stream.DangerousGetHandle ());
|
|
} finally {
|
|
if (release)
|
|
stream.DangerousRelease ();
|
|
}
|
|
}
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
static extern int ReadZStream (IntPtr stream, IntPtr buffer, int length);
|
|
|
|
static int ReadZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length)
|
|
{
|
|
bool release = false;
|
|
try {
|
|
stream.DangerousAddRef (ref release);
|
|
return ReadZStream (stream.DangerousGetHandle (), buffer, length);
|
|
} finally {
|
|
if (release)
|
|
stream.DangerousRelease ();
|
|
}
|
|
}
|
|
|
|
[MethodImplAttribute(MethodImplOptions.InternalCall)]
|
|
static extern int WriteZStream (IntPtr stream, IntPtr buffer, int length);
|
|
|
|
static int WriteZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length)
|
|
{
|
|
bool release = false;
|
|
try {
|
|
stream.DangerousAddRef (ref release);
|
|
return WriteZStream (stream.DangerousGetHandle (), buffer, length);
|
|
} finally {
|
|
if (release)
|
|
stream.DangerousRelease ();
|
|
}
|
|
}
|
|
#else
|
|
[DllImport ("MonoPosixHelper", CallingConvention=CallingConvention.Cdecl)]
|
|
static extern SafeDeflateStreamHandle CreateZStream (CompressionMode compress, bool gzip, UnmanagedReadOrWrite feeder, IntPtr data);
|
|
|
|
[DllImport ("MonoPosixHelper", CallingConvention=CallingConvention.Cdecl)]
|
|
static extern int CloseZStream (IntPtr stream);
|
|
|
|
[DllImport ("MonoPosixHelper", CallingConvention=CallingConvention.Cdecl)]
|
|
static extern int Flush (SafeDeflateStreamHandle stream);
|
|
|
|
[DllImport ("MonoPosixHelper", CallingConvention=CallingConvention.Cdecl)]
|
|
static extern int ReadZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length);
|
|
|
|
[DllImport ("MonoPosixHelper", CallingConvention=CallingConvention.Cdecl)]
|
|
static extern int WriteZStream (SafeDeflateStreamHandle stream, IntPtr buffer, int length);
|
|
#endif
|
|
|
|
sealed class SafeDeflateStreamHandle : SafeHandle
|
|
{
|
|
public override bool IsInvalid
|
|
{
|
|
get { return handle == IntPtr.Zero; }
|
|
}
|
|
|
|
private SafeDeflateStreamHandle() : base(IntPtr.Zero, true)
|
|
{
|
|
}
|
|
|
|
internal SafeDeflateStreamHandle(IntPtr handle) : base (handle, true)
|
|
{
|
|
}
|
|
|
|
override protected bool ReleaseHandle()
|
|
{
|
|
try {
|
|
DeflateStreamNative.CloseZStream(handle);
|
|
} catch {
|
|
;
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|