Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active March 2, 2021 14:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yasirkula/a066146a5cb5f2f094b6e0d37525e097 to your computer and use it in GitHub Desktop.
Save yasirkula/a066146a5cb5f2f094b6e0d37525e097 to your computer and use it in GitHub Desktop.
Automatically extract classes.jar file from Android Studio's .aar archive in Unity (for native Android plugin developers)
// Uses ZipStorer (c) 2016 Jaime Olivares [v3.4.0; August 4, 2017] (MIT-License: https://github.com/jaime-olivares/zipstorer/blob/master/LICENSE.md)
using System.Collections.Generic;
using System.Text;
using System;
using System.IO;
using System.IO.Compression;
using UnityEditor;
using UnityEngine;
namespace PluginExtractorNamespace
{
public class PluginJARExtractor
{
private const string SOURCE_AAR_PATH = @"C:\PATH\TO\ANDROID\STUDIO\PROJECT\app\build\outputs\aar\app-debug.aar";
private const string TARGET_ARCHIVE_PATH = @"Assets/Plugins/Android/MyAndroidPlugin.jar";
private static readonly List<string> VALID_IF_STARTS_WITH = new List<string>() { };
private static readonly HashSet<string> VALID_FILES = new HashSet<string>() { };
[MenuItem( "Util/Extract classes.jar from AAR", priority = 31 )]
public static void ExtractJar()
{
if( !File.Exists( SOURCE_AAR_PATH ) )
{
Debug.LogError( "Source .aar file doesn't exist" );
return;
}
string classesExtractPath = Path.Combine( Path.GetDirectoryName( SOURCE_AAR_PATH ), "classes.jar" );
// Extract classes.jar from source AAR file
ZipStorer zip = ZipStorer.Open( SOURCE_AAR_PATH, FileAccess.Read );
foreach( ZipStorer.ZipFileEntry entry in zip.ReadCentralDir() )
{
if( entry.FilenameInZip == "classes.jar" )
{
zip.ExtractFile( entry, classesExtractPath );
break;
}
}
zip.Close();
// Remove unwanted files from classes.jar
List<ZipStorer.ZipFileEntry> removeList = new List<ZipStorer.ZipFileEntry>();
zip = ZipStorer.Open( classesExtractPath, FileAccess.Read );
foreach( ZipStorer.ZipFileEntry entry in zip.ReadCentralDir() )
{
if( VALID_FILES.Contains( entry.FilenameInZip ) )
continue;
if( VALID_IF_STARTS_WITH.Count > 0 )
{
bool isFileValid = false;
foreach( string validPrefix in VALID_IF_STARTS_WITH )
{
if( entry.FilenameInZip.StartsWith( validPrefix ) )
{
isFileValid = true;
break;
}
}
if( !isFileValid )
removeList.Add( entry );
}
else if( VALID_FILES.Count > 0 || entry.FilenameInZip.EndsWith( "/BuildConfig.class" ) )
removeList.Add( entry );
}
ZipStorer.RemoveEntries( ref zip, removeList );
zip.Close();
// Move classes.jar to target path
if( !TARGET_ARCHIVE_PATH.EndsWith( ".aar", StringComparison.OrdinalIgnoreCase ) )
{
// Simply replace the target .jar file with classes.jar
File.Copy( classesExtractPath, TARGET_ARCHIVE_PATH, true );
}
else
{
// Replace the classes.jar file inside the target .aar archive with the newly extracted classes.jar file
if( !File.Exists( TARGET_ARCHIVE_PATH ) )
zip = ZipStorer.Create( TARGET_ARCHIVE_PATH, "" );
else
{
zip = ZipStorer.Open( TARGET_ARCHIVE_PATH, FileAccess.ReadWrite );
// Remove old classes.jar from that .aar, if exists
foreach( ZipStorer.ZipFileEntry entry in zip.ReadCentralDir() )
{
if( entry.FilenameInZip == "classes.jar" )
{
ZipStorer.RemoveEntries( ref zip, new List<ZipStorer.ZipFileEntry>( 1 ) { entry } );
break;
}
}
}
zip.AddFile( ZipStorer.Compression.Deflate, classesExtractPath, "classes.jar", "" );
zip.Close();
}
// We don't need the extracted classes.jar anymore
File.Delete( classesExtractPath );
// Notify Unity of the changes
AssetDatabase.ImportAsset( TARGET_ARCHIVE_PATH, ImportAssetOptions.ForceUpdate );
Debug.Log( "<b>DONE: </b>" + TARGET_ARCHIVE_PATH, AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( TARGET_ARCHIVE_PATH ) );
}
}
#region ZipStorer
/** ZipStorer
** Copyright (c) 2016 Jaime Olivares
**
** 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.
**/
/// <summary>
/// Unique class for compression/decompression file. Represents a Zip file.
/// </summary>
public class ZipStorer : IDisposable
{
/// <summary>
/// Compression method enumeration
/// </summary>
public enum Compression : ushort
{
/// <summary>Uncompressed storage</summary>
Store = 0,
/// <summary>Deflate compression method</summary>
Deflate = 8
}
/// <summary>
/// Represents an entry in Zip file directory
/// </summary>
public struct ZipFileEntry
{
/// <summary>Compression method</summary>
public Compression Method;
/// <summary>Full path and filename as stored in Zip</summary>
public string FilenameInZip;
/// <summary>Original file size</summary>
public uint FileSize;
/// <summary>Compressed file size</summary>
public uint CompressedSize;
/// <summary>Offset of header information inside Zip storage</summary>
public uint HeaderOffset;
/// <summary>Offset of file inside Zip storage</summary>
public uint FileOffset;
/// <summary>Size of header information</summary>
public uint HeaderSize;
/// <summary>32-bit checksum of entire file</summary>
public uint Crc32;
/// <summary>Last modification time of file</summary>
public DateTime ModifyTime;
/// <summary>User comment for file</summary>
public string Comment;
/// <summary>True if UTF8 encoding for filename and comments, false if default (CP 437)</summary>
public bool EncodeUTF8;
/// <summary>Overriden method</summary>
/// <returns>Filename in Zip</returns>
public override string ToString()
{
return this.FilenameInZip;
}
}
#region Public fields
/// <summary>True if UTF8 encoding for filename and comments, false if default (CP 437)</summary>
public bool EncodeUTF8 = false;
/// <summary>Force deflate algotithm even if it inflates the stored file. Off by default.</summary>
public bool ForceDeflating = false;
#endregion
#region Private fields
// List of files to store
private List<ZipFileEntry> Files = new List<ZipFileEntry>();
// Filename of storage file
private string FileName;
// Stream object of storage file
private Stream ZipFileStream;
// General comment
private string Comment = "";
// Central dir image
private byte[] CentralDirImage = null;
// Existing files in zip
private ushort ExistingFiles = 0;
// File access for Open method
private FileAccess Access;
// leave the stream open after the ZipStorer object is disposed
private bool leaveOpen;
// Static CRC32 Table
private static UInt32[] CrcTable = null;
// Default filename encoder
private static Encoding DefaultEncoding = Encoding.GetEncoding( 437 );
#endregion
#region Public methods
// Static constructor. Just invoked once in order to create the CRC32 lookup table.
static ZipStorer()
{
// Generate CRC32 table
CrcTable = new UInt32[256];
for( int i = 0; i < CrcTable.Length; i++ )
{
UInt32 c = (UInt32) i;
for( int j = 0; j < 8; j++ )
{
if( ( c & 1 ) != 0 )
c = 3988292384 ^ ( c >> 1 );
else
c >>= 1;
}
CrcTable[i] = c;
}
}
/// <summary>
/// Method to create a new storage file
/// </summary>
/// <param name="_filename">Full path of Zip file to create</param>
/// <param name="_comment">General comment for Zip file</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipStorer Create( string _filename, string _comment )
{
Stream stream = new FileStream( _filename, FileMode.Create, FileAccess.ReadWrite );
ZipStorer zip = Create( stream, _comment );
zip.Comment = _comment;
zip.FileName = _filename;
return zip;
}
/// <summary>
/// Method to create a new zip storage in a stream
/// </summary>
/// <param name="_stream"></param>
/// <param name="_comment"></param>
/// <param name="_leaveOpen">true to leave the stream open after the ZipStorer object is disposed; otherwise, false (default).</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipStorer Create( Stream _stream, string _comment, bool _leaveOpen = false )
{
ZipStorer zip = new ZipStorer()
{
Comment = _comment,
ZipFileStream = _stream,
Access = FileAccess.Write,
leaveOpen = _leaveOpen
};
return zip;
}
/// <summary>
/// Method to open an existing storage file
/// </summary>
/// <param name="_filename">Full path of Zip file to open</param>
/// <param name="_access">File access mode as used in FileStream constructor</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipStorer Open( string _filename, FileAccess _access )
{
Stream stream = (Stream) new FileStream( _filename, FileMode.Open, _access == FileAccess.Read ? FileAccess.Read : FileAccess.ReadWrite );
ZipStorer zip = Open( stream, _access );
zip.FileName = _filename;
return zip;
}
/// <summary>
/// Method to open an existing storage from stream
/// </summary>
/// <param name="_stream">Already opened stream with zip contents</param>
/// <param name="_access">File access mode for stream operations</param>
/// <param name="_leaveOpen">true to leave the stream open after the ZipStorer object is disposed; otherwise, false (default).</param>
/// <returns>A valid ZipStorer object</returns>
public static ZipStorer Open( Stream _stream, FileAccess _access, bool _leaveOpen = false )
{
if( !_stream.CanSeek && _access != FileAccess.Read )
throw new InvalidOperationException( "Stream cannot seek" );
ZipStorer zip = new ZipStorer()
{
//zip.FileName = _filename;
ZipFileStream = _stream,
Access = _access,
leaveOpen = _leaveOpen
};
if( zip.ReadFileInfo() )
return zip;
throw new System.IO.InvalidDataException();
}
/// <summary>
/// Add full contents of a file into the Zip storage
/// </summary>
/// <param name="_method">Compression method</param>
/// <param name="_pathname">Full path of file to add to Zip storage</param>
/// <param name="_filenameInZip">Filename and path as desired in Zip directory</param>
/// <param name="_comment">Comment for stored file</param>
public void AddFile( Compression _method, string _pathname, string _filenameInZip, string _comment )
{
if( Access == FileAccess.Read )
throw new InvalidOperationException( "Writing is not alowed" );
using( var stream = new FileStream( _pathname, FileMode.Open, FileAccess.Read ) )
{
AddStream( _method, _filenameInZip, stream, File.GetLastWriteTime( _pathname ), _comment );
}
}
/// <summary>
/// Add full contents of a stream into the Zip storage
/// </summary>
/// <param name="_method">Compression method</param>
/// <param name="_filenameInZip">Filename and path as desired in Zip directory</param>
/// <param name="_source">Stream object containing the data to store in Zip</param>
/// <param name="_modTime">Modification time of the data to store</param>
/// <param name="_comment">Comment for stored file</param>
public void AddStream( Compression _method, string _filenameInZip, Stream _source, DateTime _modTime, string _comment )
{
if( Access == FileAccess.Read )
throw new InvalidOperationException( "Writing is not alowed" );
// Prepare the fileinfo
ZipFileEntry zfe = new ZipFileEntry()
{
Method = _method,
EncodeUTF8 = this.EncodeUTF8,
FilenameInZip = NormalizedFilename( _filenameInZip ),
Comment = _comment ?? "",
// Even though we write the header now, it will have to be rewritten, since we don't know compressed size or crc.
Crc32 = 0, // to be updated later
HeaderOffset = (uint) this.ZipFileStream.Position, // offset within file of the start of this local record
ModifyTime = _modTime
};
// Write local header
WriteLocalHeader( ref zfe );
zfe.FileOffset = (uint) this.ZipFileStream.Position;
// Write file to zip (store)
Store( ref zfe, _source );
_source.Close();
this.UpdateCrcAndSizes( ref zfe );
Files.Add( zfe );
}
/// <summary>
/// Updates central directory (if pertinent) and close the Zip storage
/// </summary>
/// <remarks>This is a required step, unless automatic dispose is used</remarks>
public void Close()
{
if( this.Access != FileAccess.Read )
{
uint centralOffset = (uint) this.ZipFileStream.Position;
uint centralSize = 0;
if( this.CentralDirImage != null )
this.ZipFileStream.Write( CentralDirImage, 0, CentralDirImage.Length );
for( int i = 0; i < Files.Count; i++ )
{
long pos = this.ZipFileStream.Position;
this.WriteCentralDirRecord( Files[i] );
centralSize += (uint) ( this.ZipFileStream.Position - pos );
}
if( this.CentralDirImage != null )
this.WriteEndRecord( centralSize + (uint) CentralDirImage.Length, centralOffset );
else
this.WriteEndRecord( centralSize, centralOffset );
}
if( this.ZipFileStream != null && !this.leaveOpen )
{
this.ZipFileStream.Flush();
this.ZipFileStream.Dispose();
this.ZipFileStream = null;
}
}
/// <summary>
/// Read all the file records in the central directory
/// </summary>
/// <returns>List of all entries in directory</returns>
public List<ZipFileEntry> ReadCentralDir()
{
if( this.CentralDirImage == null )
throw new InvalidOperationException( "Central directory currently does not exist" );
List<ZipFileEntry> result = new List<ZipFileEntry>();
for( int pointer = 0; pointer < this.CentralDirImage.Length; )
{
uint signature = BitConverter.ToUInt32( CentralDirImage, pointer );
if( signature != 0x02014b50 )
break;
bool encodeUTF8 = ( BitConverter.ToUInt16( CentralDirImage, pointer + 8 ) & 0x0800 ) != 0;
ushort method = BitConverter.ToUInt16( CentralDirImage, pointer + 10 );
uint modifyTime = BitConverter.ToUInt32( CentralDirImage, pointer + 12 );
uint crc32 = BitConverter.ToUInt32( CentralDirImage, pointer + 16 );
uint comprSize = BitConverter.ToUInt32( CentralDirImage, pointer + 20 );
uint fileSize = BitConverter.ToUInt32( CentralDirImage, pointer + 24 );
ushort filenameSize = BitConverter.ToUInt16( CentralDirImage, pointer + 28 );
ushort extraSize = BitConverter.ToUInt16( CentralDirImage, pointer + 30 );
ushort commentSize = BitConverter.ToUInt16( CentralDirImage, pointer + 32 );
uint headerOffset = BitConverter.ToUInt32( CentralDirImage, pointer + 42 );
uint headerSize = (uint) ( 46 + filenameSize + extraSize + commentSize );
Encoding encoder = encodeUTF8 ? Encoding.UTF8 : DefaultEncoding;
ZipFileEntry zfe = new ZipFileEntry()
{
Method = (Compression) method,
FilenameInZip = encoder.GetString( CentralDirImage, pointer + 46, filenameSize ),
FileOffset = GetFileOffset( headerOffset ),
FileSize = fileSize,
CompressedSize = comprSize,
HeaderOffset = headerOffset,
HeaderSize = headerSize,
Crc32 = crc32,
ModifyTime = DosTimeToDateTime( modifyTime ) ?? DateTime.Now
};
if( commentSize > 0 )
zfe.Comment = encoder.GetString( CentralDirImage, pointer + 46 + filenameSize + extraSize, commentSize );
result.Add( zfe );
pointer += ( 46 + filenameSize + extraSize + commentSize );
}
return result;
}
/// <summary>
/// Copy the contents of a stored file into a physical file
/// </summary>
/// <param name="_zfe">Entry information of file to extract</param>
/// <param name="_filename">Name of file to store uncompressed data</param>
/// <returns>True if success, false if not.</returns>
/// <remarks>Unique compression methods are Store and Deflate</remarks>
public bool ExtractFile( ZipFileEntry _zfe, string _filename )
{
// Make sure the parent directory exist
string path = Path.GetDirectoryName( _filename );
if( !Directory.Exists( path ) )
Directory.CreateDirectory( path );
// Check it is directory. If so, do nothing
if( Directory.Exists( _filename ) )
return true;
bool result;
using( var output = new FileStream( _filename, FileMode.Create, FileAccess.Write ) )
{
result = ExtractFile( _zfe, output );
}
if( result )
{
File.SetCreationTime( _filename, _zfe.ModifyTime );
File.SetLastWriteTime( _filename, _zfe.ModifyTime );
}
return result;
}
/// <summary>
/// Copy the contents of a stored file into an opened stream
/// </summary>
/// <param name="_zfe">Entry information of file to extract</param>
/// <param name="_stream">Stream to store the uncompressed data</param>
/// <returns>True if success, false if not.</returns>
/// <remarks>Unique compression methods are Store and Deflate</remarks>
public bool ExtractFile( ZipFileEntry _zfe, Stream _stream )
{
if( !_stream.CanWrite )
throw new InvalidOperationException( "Stream cannot be written" );
// check signature
byte[] signature = new byte[4];
this.ZipFileStream.Seek( _zfe.HeaderOffset, SeekOrigin.Begin );
this.ZipFileStream.Read( signature, 0, 4 );
if( BitConverter.ToUInt32( signature, 0 ) != 0x04034b50 )
return false;
// Select input stream for inflating or just reading
Stream inStream;
if( _zfe.Method == Compression.Store )
inStream = this.ZipFileStream;
else if( _zfe.Method == Compression.Deflate )
inStream = new DeflateStream( this.ZipFileStream, CompressionMode.Decompress, true );
else
return false;
// Buffered copy
byte[] buffer = new byte[16384];
this.ZipFileStream.Seek( _zfe.FileOffset, SeekOrigin.Begin );
uint bytesPending = _zfe.FileSize;
while( bytesPending > 0 )
{
int bytesRead = inStream.Read( buffer, 0, (int) Math.Min( bytesPending, buffer.Length ) );
_stream.Write( buffer, 0, bytesRead );
bytesPending -= (uint) bytesRead;
}
_stream.Flush();
if( _zfe.Method == Compression.Deflate )
inStream.Dispose();
return true;
}
/// <summary>
/// Copy the contents of a stored file into a byte array
/// </summary>
/// <param name="_zfe">Entry information of file to extract</param>
/// <param name="_file">Byte array with uncompressed data</param>
/// <returns>True if success, false if not.</returns>
/// <remarks>Unique compression methods are Store and Deflate</remarks>
public bool ExtractFile( ZipFileEntry _zfe, out byte[] _file )
{
using( MemoryStream ms = new MemoryStream() )
{
if( ExtractFile( _zfe, ms ) )
{
_file = ms.ToArray();
return true;
}
else
{
_file = null;
return false;
}
}
}
/// <summary>
/// Removes one of many files in storage. It creates a new Zip file.
/// </summary>
/// <param name="_zip">Reference to the current Zip object</param>
/// <param name="_zfes">List of Entries to remove from storage</param>
/// <returns>True if success, false if not</returns>
/// <remarks>This method only works for storage of type FileStream</remarks>
public static bool RemoveEntries( ref ZipStorer _zip, List<ZipFileEntry> _zfes )
{
if( !( _zip.ZipFileStream is FileStream ) )
throw new InvalidOperationException( "RemoveEntries is allowed just over streams of type FileStream" );
//Get full list of entries
var fullList = _zip.ReadCentralDir();
//In order to delete we need to create a copy of the zip file excluding the selected items
var tempZipName = Path.GetTempFileName();
var tempEntryName = Path.GetTempFileName();
try
{
var tempZip = ZipStorer.Create( tempZipName, string.Empty );
foreach( ZipFileEntry zfe in fullList )
{
if( !_zfes.Contains( zfe ) && ( zfe.FileSize != 0 || !zfe.FilenameInZip.EndsWith( "/" ) ) )
{
if( _zip.ExtractFile( zfe, tempEntryName ) )
{
tempZip.AddFile( zfe.Method, tempEntryName, zfe.FilenameInZip, zfe.Comment );
}
}
}
_zip.Close();
tempZip.Close();
File.Delete( _zip.FileName );
File.Move( tempZipName, _zip.FileName );
_zip = ZipStorer.Open( _zip.FileName, _zip.Access );
}
catch
{
return false;
}
finally
{
if( File.Exists( tempZipName ) )
File.Delete( tempZipName );
if( File.Exists( tempEntryName ) )
File.Delete( tempEntryName );
}
return true;
}
#endregion
#region Private methods
// Calculate the file offset by reading the corresponding local header
private uint GetFileOffset( uint _headerOffset )
{
byte[] buffer = new byte[2];
this.ZipFileStream.Seek( _headerOffset + 26, SeekOrigin.Begin );
this.ZipFileStream.Read( buffer, 0, 2 );
ushort filenameSize = BitConverter.ToUInt16( buffer, 0 );
this.ZipFileStream.Read( buffer, 0, 2 );
ushort extraSize = BitConverter.ToUInt16( buffer, 0 );
return (uint) ( 30 + filenameSize + extraSize + _headerOffset );
}
/* Local file header:
local file header signature 4 bytes (0x04034b50)
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
filename length 2 bytes
extra field length 2 bytes
filename (variable size)
extra field (variable size)
*/
private void WriteLocalHeader( ref ZipFileEntry _zfe )
{
long pos = this.ZipFileStream.Position;
Encoding encoder = _zfe.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding;
byte[] encodedFilename = encoder.GetBytes( _zfe.FilenameInZip );
this.ZipFileStream.Write( new byte[] { 80, 75, 3, 4, 20, 0 }, 0, 6 ); // No extra header
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) ( _zfe.EncodeUTF8 ? 0x0800 : 0 ) ), 0, 2 ); // filename and comment encoding
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) _zfe.Method ), 0, 2 ); // zipping method
this.ZipFileStream.Write( BitConverter.GetBytes( DateTimeToDosTime( _zfe.ModifyTime ) ), 0, 4 ); // zipping date and time
this.ZipFileStream.Write( new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0, 12 ); // unused CRC, un/compressed size, updated later
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) encodedFilename.Length ), 0, 2 ); // filename length
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0 ), 0, 2 ); // extra length
this.ZipFileStream.Write( encodedFilename, 0, encodedFilename.Length );
_zfe.HeaderSize = (uint) ( this.ZipFileStream.Position - pos );
}
/* Central directory's File header:
central file header signature 4 bytes (0x02014b50)
version made by 2 bytes
version needed to extract 2 bytes
general purpose bit flag 2 bytes
compression method 2 bytes
last mod file time 2 bytes
last mod file date 2 bytes
crc-32 4 bytes
compressed size 4 bytes
uncompressed size 4 bytes
filename length 2 bytes
extra field length 2 bytes
file comment length 2 bytes
disk number start 2 bytes
internal file attributes 2 bytes
external file attributes 4 bytes
relative offset of local header 4 bytes
filename (variable size)
extra field (variable size)
file comment (variable size)
*/
private void WriteCentralDirRecord( ZipFileEntry _zfe )
{
Encoding encoder = _zfe.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding;
byte[] encodedFilename = encoder.GetBytes( _zfe.FilenameInZip );
byte[] encodedComment = encoder.GetBytes( _zfe.Comment );
this.ZipFileStream.Write( new byte[] { 80, 75, 1, 2, 23, 0xB, 20, 0 }, 0, 8 );
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) ( _zfe.EncodeUTF8 ? 0x0800 : 0 ) ), 0, 2 ); // filename and comment encoding
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) _zfe.Method ), 0, 2 ); // zipping method
this.ZipFileStream.Write( BitConverter.GetBytes( DateTimeToDosTime( _zfe.ModifyTime ) ), 0, 4 ); // zipping date and time
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.Crc32 ), 0, 4 ); // file CRC
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.CompressedSize ), 0, 4 ); // compressed file size
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.FileSize ), 0, 4 ); // uncompressed file size
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) encodedFilename.Length ), 0, 2 ); // Filename in zip
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0 ), 0, 2 ); // extra length
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) encodedComment.Length ), 0, 2 );
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0 ), 0, 2 ); // disk=0
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0 ), 0, 2 ); // file type: binary
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0 ), 0, 2 ); // Internal file attributes
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) 0x8100 ), 0, 2 ); // External file attributes (normal/readable)
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.HeaderOffset ), 0, 4 ); // Offset of header
this.ZipFileStream.Write( encodedFilename, 0, encodedFilename.Length );
this.ZipFileStream.Write( encodedComment, 0, encodedComment.Length );
}
/* End of central dir record:
end of central dir signature 4 bytes (0x06054b50)
number of this disk 2 bytes
number of the disk with the
start of the central directory 2 bytes
total number of entries in
the central dir on this disk 2 bytes
total number of entries in
the central dir 2 bytes
size of the central directory 4 bytes
offset of start of central
directory with respect to
the starting disk number 4 bytes
zipfile comment length 2 bytes
zipfile comment (variable size)
*/
private void WriteEndRecord( uint _size, uint _offset )
{
Encoding encoder = this.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding;
byte[] encodedComment = encoder.GetBytes( this.Comment );
this.ZipFileStream.Write( new byte[] { 80, 75, 5, 6, 0, 0, 0, 0 }, 0, 8 );
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) Files.Count + ExistingFiles ), 0, 2 );
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) Files.Count + ExistingFiles ), 0, 2 );
this.ZipFileStream.Write( BitConverter.GetBytes( _size ), 0, 4 );
this.ZipFileStream.Write( BitConverter.GetBytes( _offset ), 0, 4 );
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) encodedComment.Length ), 0, 2 );
this.ZipFileStream.Write( encodedComment, 0, encodedComment.Length );
}
// Copies all source file into storage file
private void Store( ref ZipFileEntry _zfe, Stream _source )
{
byte[] buffer = new byte[16384];
int bytesRead;
uint totalRead = 0;
Stream outStream;
long posStart = this.ZipFileStream.Position;
long sourceStart = _source.CanSeek ? _source.Position : 0;
if( _zfe.Method == Compression.Store )
outStream = this.ZipFileStream;
else
outStream = new DeflateStream( this.ZipFileStream, CompressionMode.Compress, true );
_zfe.Crc32 = 0 ^ 0xffffffff;
do
{
bytesRead = _source.Read( buffer, 0, buffer.Length );
totalRead += (uint) bytesRead;
if( bytesRead > 0 )
{
outStream.Write( buffer, 0, bytesRead );
for( uint i = 0; i < bytesRead; i++ )
{
_zfe.Crc32 = ZipStorer.CrcTable[( _zfe.Crc32 ^ buffer[i] ) & 0xFF] ^ ( _zfe.Crc32 >> 8 );
}
}
} while( bytesRead > 0 );
outStream.Flush();
if( _zfe.Method == Compression.Deflate )
outStream.Dispose();
_zfe.Crc32 ^= 0xffffffff;
_zfe.FileSize = totalRead;
_zfe.CompressedSize = (uint) ( this.ZipFileStream.Position - posStart );
// Verify for real compression
if( _zfe.Method == Compression.Deflate && !this.ForceDeflating && _source.CanSeek && _zfe.CompressedSize > _zfe.FileSize )
{
// Start operation again with Store algorithm
_zfe.Method = Compression.Store;
this.ZipFileStream.Position = posStart;
this.ZipFileStream.SetLength( posStart );
_source.Position = sourceStart;
this.Store( ref _zfe, _source );
}
}
/* DOS Date and time:
MS-DOS date. The date is a packed value with the following format. Bits Description
0-4 Day of the month (1–31)
5-8 Month (1 = January, 2 = February, and so on)
9-15 Year offset from 1980 (add 1980 to get actual year)
MS-DOS time. The time is a packed value with the following format. Bits Description
0-4 Second divided by 2
5-10 Minute (0–59)
11-15 Hour (0–23 on a 24-hour clock)
*/
private uint DateTimeToDosTime( DateTime _dt )
{
return (uint) (
( _dt.Second / 2 ) | ( _dt.Minute << 5 ) | ( _dt.Hour << 11 ) |
( _dt.Day << 16 ) | ( _dt.Month << 21 ) | ( ( _dt.Year - 1980 ) << 25 ) );
}
private DateTime? DosTimeToDateTime( uint _dt )
{
int year = (int) ( _dt >> 25 ) + 1980;
int month = (int) ( _dt >> 21 ) & 15;
int day = (int) ( _dt >> 16 ) & 31;
int hours = (int) ( _dt >> 11 ) & 31;
int minutes = (int) ( _dt >> 5 ) & 63;
int seconds = (int) ( _dt & 31 ) * 2;
if( month == 0 || day == 0 )
return null;
return new DateTime( year, month, day, hours, minutes, seconds );
}
/* CRC32 algorithm
The 'magic number' for the CRC is 0xdebb20e3.
The proper CRC pre and post conditioning is used, meaning that the CRC register is
pre-conditioned with all ones (a starting value of 0xffffffff) and the value is post-conditioned by
taking the one's complement of the CRC residual.
If bit 3 of the general purpose flag is set, this field is set to zero in the local header and the correct
value is put in the data descriptor and in the central directory.
*/
private void UpdateCrcAndSizes( ref ZipFileEntry _zfe )
{
long lastPos = this.ZipFileStream.Position; // remember position
this.ZipFileStream.Position = _zfe.HeaderOffset + 8;
this.ZipFileStream.Write( BitConverter.GetBytes( (ushort) _zfe.Method ), 0, 2 ); // zipping method
this.ZipFileStream.Position = _zfe.HeaderOffset + 14;
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.Crc32 ), 0, 4 ); // Update CRC
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.CompressedSize ), 0, 4 ); // Compressed size
this.ZipFileStream.Write( BitConverter.GetBytes( _zfe.FileSize ), 0, 4 ); // Uncompressed size
this.ZipFileStream.Position = lastPos; // restore position
}
// Replaces backslashes with slashes to store in zip header
private string NormalizedFilename( string _filename )
{
string filename = _filename.Replace( '\\', '/' );
int pos = filename.IndexOf( ':' );
if( pos >= 0 )
filename = filename.Remove( 0, pos + 1 );
return filename.Trim( '/' );
}
// Reads the end-of-central-directory record
private bool ReadFileInfo()
{
if( this.ZipFileStream.Length < 22 )
return false;
try
{
this.ZipFileStream.Seek( -17, SeekOrigin.End );
BinaryReader br = new BinaryReader( this.ZipFileStream );
do
{
this.ZipFileStream.Seek( -5, SeekOrigin.Current );
UInt32 sig = br.ReadUInt32();
if( sig == 0x06054b50 )
{
this.ZipFileStream.Seek( 6, SeekOrigin.Current );
UInt16 entries = br.ReadUInt16();
Int32 centralSize = br.ReadInt32();
UInt32 centralDirOffset = br.ReadUInt32();
UInt16 commentSize = br.ReadUInt16();
// check if comment field is the very last data in file
if( this.ZipFileStream.Position + commentSize != this.ZipFileStream.Length )
return false;
// Copy entire central directory to a memory buffer
this.ExistingFiles = entries;
this.CentralDirImage = new byte[centralSize];
this.ZipFileStream.Seek( centralDirOffset, SeekOrigin.Begin );
this.ZipFileStream.Read( this.CentralDirImage, 0, centralSize );
// Leave the pointer at the begining of central dir, to append new files
this.ZipFileStream.Seek( centralDirOffset, SeekOrigin.Begin );
return true;
}
} while( this.ZipFileStream.Position > 0 );
}
catch { }
return false;
}
#endregion
#region IDisposable Members
/// <summary>
/// Closes the Zip file stream
/// </summary>
public void Dispose()
{
this.Close();
}
#endregion
}
#endregion
}
@yasirkula
Copy link
Author

yasirkula commented Jul 26, 2019

Usage

  1. Add PluginJARExtractor.cs to your Unity project's Editor folder inside Project view (create that folder if it doesn't exist)
  2. Fill in the SOURCE_AAR_PATH constant so that it points to the .aar output of your Android Studio project
  3. Click the Util-Extract classes.jar from AAR menu button to extract the .jar to TARGET_ARCHIVE_PATH inside your Unity project:
    • If target path has .jar extension, classes.jar will simply be moved there
    • Otherwise (target path has .aar extension), target archive's classes.jar will be replaced with the new classes.jar file

Filtering Files

  • By default, only the BuildConfig.class file is removed from classes.jar since, AFAIK, BuildConfig is not needed in plugins. Also, if multiple native plugins have BuildConfig.class in their .jar file, it will result in a compilation error; so this is another good reason to omit BuildConfig
  • If you add some filepaths to VALID_FILES and/or VALID_IF_STARTS_WITH, only those specified files are included in classes.jar as follows:
    • VALID_FILES: files at these paths are included in classes.jar (e.g. "com/yasirkula/unity/NativeCameraUtils.class")
    • VALID_IF_STARTS_WITH: files whose paths start with these paths are included in classes.jar (e.g. adding "com/yasirkula/unity/NativeCamera" will include any files whose paths start with that string, like "com/yasirkula/unity/NativeCameraUtils.class" and "com/yasirkula/unity/NativeCameraPictureFragment.class")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment