Example #1
0
        /// <summary>
        /// Saves the given root element to the given stream as AXML.
        /// </summary>
        /// <param name="stream">Stream to save to</param>
        /// <param name="rootElement">Root element of the document</param>
        public static void SaveDocument(Stream stream, AxmlElement rootElement)
        {
            BinaryWriter mainOutput = new BinaryWriter(stream);

            // Write the main elements chunk of the file to a MemoryStream first
            SavingContext ctx = new SavingContext();

            rootElement.PrepareResourceIndices(ctx);
            rootElement.Save(ctx);
            MemoryStream mainChunkStream = (MemoryStream)ctx.Writer.BaseStream;


            string[] stringPool   = ctx.StringPool.Save();
            int[]    resourcePool = ctx.ResourcePool.Save();

            int stringPoolLength  = StringPool.CalculatePoolLength(stringPool);
            int stringPoolPadding = 4 - stringPoolLength % 4;

            stringPoolLength += stringPoolPadding;            // Add padding to four bytes

            int resourcePoolLength = resourcePool.Length * 4; // Each pool item is an integer

            // The length of the main xml tag is that of the whole file, so also including the string pool, resource pool, and main chunk. (+ extra 8 + 8 = 16 bytes for string pool and resource pool header)
            mainOutput.WriteChunkHeader(ResourceType.Xml, stringPoolLength + resourcePoolLength + (int)mainChunkStream.Position + 16);

            mainOutput.WriteChunkHeader(ResourceType.StringPool, stringPoolLength);
            StringPool.SaveStringPool(stringPool, mainOutput);
            for (int i = 0; i < stringPoolPadding; i++)
            {
                mainOutput.Write((byte)0);
            }

            mainOutput.WriteChunkHeader(ResourceType.XmlResourceMap, resourcePoolLength);
            foreach (int resource in resourcePool)
            {
                mainOutput.Write(resource);
            }

            // Save the main chunk of the file
            mainChunkStream.Position = 0;
            mainChunkStream.CopyTo(stream);
        }
Example #2
0
        /// <summary>
        /// Loads an AXML document from the given stream.
        /// The stream must be seekable.
        /// </summary>
        /// <param name="stream">The stream to load from, must be seekable</param>
        /// <returns></returns>
        /// <exception cref="ArgumentException">If the given stream is not seekable</exception>
        /// <exception cref="AxmlParseException">Any errors in the AXML format of the file</exception>
        public static AxmlElement LoadDocument(Stream stream)
        {
            if (!stream.CanSeek)
            {
                throw new ArgumentException("Cannot read axml from non-seekable stream");
            }

            BinaryReader input = new BinaryReader(stream);

            if (input.ReadResourceType() != ResourceType.Xml)
            {
                throw new AxmlParseException("Initial tag was not xml");
            }

            int fileSize = input.ReadInt32();

            string[]? stringPool = null;
            int[]? resourceMap   = null;

            Stack <AxmlElement>    elementStack     = new Stack <AxmlElement>();
            List <QueuedNamespace> queuedNamespaces = new List <QueuedNamespace>();
            AxmlElement?           rootElement      = null;

            int preChunkPosition = 8; // Already gone past two ints for initial XML tag and file size

            while (preChunkPosition < fileSize)
            {
                ResourceType chunkType   = input.ReadResourceType();
                int          chunkLength = input.ReadInt32();

                if (stringPool == null && chunkType != ResourceType.StringPool)
                {
                    throw new AxmlParseException("String pool must be located after Xml tag");
                }


                int currentLineNumber;
                switch (chunkType)
                {
                // The string pool must come before any elements, a check for this is above
                case ResourceType.StringPool:
                    stringPool = StringPool.LoadStringPool(input);
                    break;

                case ResourceType.XmlResourceMap:
                    // Divide by 4 because the resource map is made up of integers, subtract 2 for the resource type and length values
                    int resourceCount = chunkLength / 4 - 2;
                    resourceMap = new int[resourceCount];
                    for (int i = 0; i < resourceCount; i++)
                    {
                        resourceMap[i] = input.ReadInt32();
                    }

                    break;

                case ResourceType.XmlStartNamespace:
                    input.ReadInt32();     // Line number, currently unused
                    if (input.ReadUInt32() != 0xFFFFFFFF)
                    {
                        throw new AxmlParseException("Expected 0xFFFFFFFF");
                    }

                    Debug.Assert(stringPool != null);
                    int    prefixId = input.ReadInt32();
                    string?prefix   = prefixId == -1 ? null : stringPool[prefixId];

                    string uriString = stringPool[input.ReadInt32()];
                    Uri    uri       = ParseNamespaceUri(uriString);

                    queuedNamespaces.Add(new QueuedNamespace(prefix, uri));
                    break;

                case ResourceType.XmlEndNamespace:
                    break;

                case ResourceType.XmlStartElement:
                    currentLineNumber = input.ReadInt32();
                    if (input.ReadUInt32() != 0xFFFFFFFF)
                    {
                        throw new AxmlParseException("Expected 0xFFFFFFFF");
                    }

                    Debug.Assert(stringPool != null);
                    int    namespaceId = input.ReadInt32();  // -1 means no namespace prefix, so default namespace
                    string elementName = stringPool[input.ReadInt32()];
                    if (input.ReadUInt32() != 0x00140014)
                    {
                        throw new AxmlParseException("Expected 0x00140014");
                    }

                    AxmlElement childElement = new AxmlElement(elementName, namespaceId == -1 ? null : ParseNamespaceUri(stringPool[namespaceId]), currentLineNumber);

                    int numAttributes       = input.ReadInt16();
                    int idAttributeIndex    = input.ReadInt16() - 1;
                    int classAttributeIndex = input.ReadInt16() - 1;
                    int styleAttributeIndex = input.ReadInt16() - 1;
                    for (int i = 0; i < numAttributes; i++)
                    {
                        int attrNamespaceId = input.ReadInt32();
                        Uri?attrNamespace   = attrNamespaceId == -1 ? null : ParseNamespaceUri(stringPool[attrNamespaceId]);

                        int attrNameAndResourceIdIndex = input.ReadInt32();

                        string attrName       = stringPool[attrNameAndResourceIdIndex];
                        int?   attrResourceId = null;

                        if (resourceMap == null)
                        {
                            throw new AxmlParseException(
                                      $"Attempted to access resource ID with index {attrNameAndResourceIdIndex} when the resource pool chunk had not yet been received");
                        }
                        if (attrNameAndResourceIdIndex >= 0 && attrNameAndResourceIdIndex < resourceMap.Length)
                        {
                            attrResourceId = resourceMap[attrNameAndResourceIdIndex];
                        }

                        int           attrRawStringIndex = input.ReadInt32();
                        AttributeType attrType           = (AttributeType)(input.ReadInt32() >> 24); // The first byte contains the actual type, so we shift this to the right
                        int           attrRawValue       = input.ReadInt32();

                        object value;
                        if (i == idAttributeIndex)
                        {
                            value = new WrappedValue(WrappedValueType.Id, stringPool[attrRawStringIndex], attrRawValue);
                        }
                        else if (i == classAttributeIndex)
                        {
                            value = new WrappedValue(WrappedValueType.Class, stringPool[attrRawStringIndex], attrRawValue);
                        }
                        else if (i == styleAttributeIndex)
                        {
                            value = new WrappedValue(WrappedValueType.Style, stringPool[attrRawStringIndex], attrRawValue);
                        }
                        else if (attrType == AttributeType.Reference)
                        {
                            value = new WrappedValue(WrappedValueType.Reference, null, attrRawValue);
                        }
                        else if (attrType == AttributeType.String)
                        {
                            value = stringPool[attrRawValue];
                        }
                        else if (attrType == AttributeType.Boolean)
                        {
                            value = attrRawValue != 0;
                        }
                        else
                        {
                            value = attrRawValue;
                        }

                        childElement.Attributes.Add(new AxmlAttribute(attrName, attrNamespace, attrResourceId, value, attrType));
                    }

                    // Add the namespaces of any parent StartNamespace resources to this element
                    foreach (QueuedNamespace ns in queuedNamespaces)
                    {
                        if (ns.Prefix == null)     // No prefix means that this is a default namespace
                        {
                            childElement.DeclaredDefaultNamespace = ns.Uri;
                        }
                        else
                        {
                            childElement.DeclaredNamespaces[ns.Prefix] = ns.Uri;
                        }
                    }
                    queuedNamespaces.Clear();

                    // Add the child element to the current bottom-most element in the stack.
                    if (elementStack.Count == 0)
                    {
                        if (rootElement != null)
                        {
                            throw new AxmlParseException("Document contained multiple root elements");
                        }
                        rootElement = childElement;
                    }
                    else
                    {
                        elementStack.Peek().Children.Add(childElement);
                    }
                    // Set this element as the bottom-most element
                    elementStack.Push(childElement);


                    break;

                case ResourceType.XmlEndElement:
                    elementStack.Pop();     // Current bottom-most element is now the next element up
                    break;

                case ResourceType.XmlCdata:
                    input.ReadInt32();     // Line number, currently unused
                    if (input.ReadUInt32() != 0xFFFFFFFF)
                    {
                        throw new AxmlParseException("Expected 0xFFFFFFFF");
                    }

                    input.ReadInt32();     // TODO: ID of "text" value. Currently unused

                    // TODO: Unused bytes. (figure out what they are)
                    input.ReadInt32();
                    input.ReadInt32();

                    break;

                default:
                    throw new AxmlParseException("Unknown chunk type: " + chunkType);
                }

                input.BaseStream.Position = preChunkPosition + chunkLength;
                preChunkPosition          = (int)input.BaseStream.Position;
            }

            if (rootElement == null)
            {
                throw new AxmlParseException("Document did not contain a root element");
            }

            return(rootElement);
        }