// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Text; using EpicGames.Core; using UnrealBuildBase; namespace UnrealBuildTool { /// /// Encapsulates metadata for a C++ source file /// [Serializable] class SourceFile { /// /// The corresponding file item /// public readonly FileItem File; /// /// Hash of the source file data /// public readonly ContentHash Hash; /// /// Last write time of this file when constructed /// public readonly long LastWriteTimeUtc; /// /// Markup dividing up this source file /// public readonly SourceFileMarkup[] Markup; /// /// Macro which is defined as part of the header guard. If this macro is defined, we don't need to include the header more than once. /// public Identifier? HeaderGuardMacro { get; private set; } /// /// Parsed fragments in this file. Only set for files which are being optimized. /// public readonly SourceFileFragment[] Fragments; /// /// Gets the location of this file /// public FileReference Location => File.Location; /// /// Construct a SourceFile from a file on disk /// /// Location of the file public SourceFile(FileItem file) : this(file, ReadFileWithNullTerminator(file.Location)) { LastWriteTimeUtc = file.LastWriteTimeUtc.Ticks; } /// /// Construct a SourceFile from a raw byte array /// /// Location of the file /// Contents of the file public SourceFile(FileItem file, byte[] data) { File = file; // Compute the hash Hash = ContentHash.MD5(data); // Read the preprocessor markup TokenReader reader = new(data); Markup = SourceFileMarkup.Parse(reader); // Try to parse a header guard from this file ParseHeaderGuard(); // Create the fragments List fragmentsList = new(); for (int maxIdx = 0; maxIdx < Markup.Length; maxIdx++) { if (Markup[maxIdx].Type != SourceFileMarkupType.Include) { int minIdx = maxIdx++; while (maxIdx < Markup.Length && Markup[maxIdx].Type != SourceFileMarkupType.Include) { maxIdx++; } fragmentsList.Add(new SourceFileFragment(this, minIdx, maxIdx)); } } Fragments = fragmentsList.ToArray(); } /// /// Read a source file from a binary reader /// /// The reader to serialize from public SourceFile(BinaryArchiveReader reader) { File = reader.ReadFileItem()!; Hash = reader.ReadContentHash()!; LastWriteTimeUtc = reader.ReadLong(); Markup = reader.ReadArray(() => new SourceFileMarkup(reader))!; HeaderGuardMacro = reader.ReadIdentifier(); Fragments = reader.ReadArray(() => new SourceFileFragment(this, reader))!; } /// /// Read a source file from a binary reader /// /// The reader to serialize to public void Write(BinaryArchiveWriter writer) { writer.WriteFileItem(File); writer.WriteContentHash(Hash); writer.WriteLong(LastWriteTimeUtc); writer.WriteArray(Markup, x => x.Write(writer)); writer.WriteIdentifier(HeaderGuardMacro); writer.WriteArray(Fragments, x => x.Write(writer)); } /// /// Reads the contents of a file into a buffer, with a null terminator /// /// Location of the file /// Array of bytes, with a null terminator public static byte[] ReadFileWithNullTerminator(FileReference location) { using FileStream stream = FileReference.Open(location, FileMode.Open, FileAccess.Read, FileShare.Read); int length = (int)stream.Length; byte[] data = new byte[length + 1]; stream.Read(data, 0, length); return ConvertToNullTerminatedUtf8(data, length); } /// /// Converts a buffer from a given encoding to UTF8, appending a null terminator /// /// The source data /// Length of the data in the source buffer /// Array of utf-8 encoded characters, with a null terminator public static byte[] ConvertToNullTerminatedUtf8(byte[] data, int length) { if (length >= 2) { if (data[0] == 0xfe && data[1] == 0xff) { return ConvertToNullTerminatedUtf8(Encoding.BigEndianUnicode, data, length); } else if (data[0] == 0xff && data[1] == 0xfe) { return ConvertToNullTerminatedUtf8(Encoding.Unicode, data, length); } } return data; } /// /// Converts a buffer from a given encoding to UTF8, appending a null terminator /// /// The encoding to convert from /// The source data /// Length of the data in the source buffer /// Array of utf-8 encoded characters, with a null terminator public static byte[] ConvertToNullTerminatedUtf8(Encoding sourceEncoding, byte[] data, int length) { byte[] newBytes = data; if (sourceEncoding is not UTF8Encoding) { char[] chars = sourceEncoding.GetChars(data, 0, length); int newLength = Encoding.UTF8.GetByteCount(chars); newBytes = new byte[newLength + 1]; Encoding.UTF8.GetBytes(chars, 0, chars.Length, newBytes, 0); } return newBytes; } /// /// Checks whether the markup matches a C-style header guard /// void ParseHeaderGuard() { // Make sure there are enough markup entries in this list if (Markup.Length >= 3) { int minIdx = 0; // Get the define used for the header guard Identifier? possibleHeaderGuardMacro; if (Markup[minIdx].Type == SourceFileMarkupType.Ifndef && Markup[minIdx].Tokens!.Count == 1 && Markup[minIdx].Tokens![0].Type == TokenType.Identifier) { possibleHeaderGuardMacro = Markup[minIdx].Tokens![0].Identifier!; } else if (Markup[minIdx].Type == SourceFileMarkupType.If && Markup[minIdx].Tokens!.Count == 3 && Markup[minIdx].Tokens![0].Type == TokenType.LogicalNot && Markup[minIdx].Tokens![1].Identifier == Identifiers.Defined && Markup[minIdx].Tokens![2].Type == TokenType.Identifier) { possibleHeaderGuardMacro = Markup[minIdx].Tokens![2].Identifier!; } else { possibleHeaderGuardMacro = null; } // Check it starts with an #if or #ifdef if (Markup[minIdx + 1].Type == SourceFileMarkupType.Define && Markup[minIdx + 1].Tokens!.Count == 1 && Markup[minIdx + 1].Tokens![0].Identifier == possibleHeaderGuardMacro) { // Find the point at which the include depth goes back to zero int maxIdx = minIdx + 1; for (int depth = 1; maxIdx < Markup.Length; maxIdx++) { depth += Markup[maxIdx].GetConditionDepthDelta(); if (depth == 0) { break; } } // Check it matched the end of the file if (maxIdx == Markup.Length - 1) { HeaderGuardMacro = possibleHeaderGuardMacro; } } } } /// /// Converts a source file to a string for debugging /// /// The full path to the file public override string ToString() { return File.ToString(); } } }