/// <summary> /// Removes a file from <see cref="Files"/> /// </summary> /// <param name="file">The file to remove</param> public void RemoveFile(ArFile file) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (file.Parent != this) { throw new InvalidOperationException($"Cannot remove this file as it is not part of this {nameof(ArArchiveFile)} instance"); } // If we are removing the SymbolTable if (file == SymbolTable) { SymbolTable = null; } var i = (int)file.Index; _files.RemoveAt(i); file.Index = 0; // Update indices for other sections for (int j = i + 1; j < _files.Count; j++) { var nextEntry = _files[j]; nextEntry.Index--; } file.Parent = null; }
/// <summary> /// Adds a file to <see cref="Files"/>. /// </summary> /// <param name="file">A file</param> public void AddFile(ArFile file) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (file.Parent != null) { if (file.Parent == this) { throw new InvalidOperationException("Cannot add the file as it is already added"); } if (file.Parent != this) { throw new InvalidOperationException($"Cannot add the file as it is already added to another {nameof(ArArchiveFile)} instance"); } } if (file is ArSymbolTable symbolTable) { InsertFileAt(0, file); return; } file.Parent = this; file.Index = (uint)_files.Count; _files.Add(file); }
/// <summary> /// Inserts a file into <see cref="Files"/> at the specified index. /// </summary> /// <param name="index">Index into <see cref="Files"/> to insert the specified file</param> /// <param name="file">The file to insert</param> public void InsertFileAt(int index, ArFile file) { if (index < 0 || index > _files.Count) { throw new ArgumentOutOfRangeException(nameof(index), $"Invalid index {index}, Must be >= 0 && <= {_files.Count}"); } if (file == null) { throw new ArgumentNullException(nameof(file)); } if (file.Parent != null) { if (file.Parent == this) { throw new InvalidOperationException("Cannot add the file as it is already added"); } if (file.Parent != this) { throw new InvalidOperationException($"Cannot add the file as it is already added to another {nameof(ArArchiveFile)} instance"); } } if (file is ArSymbolTable symbolTable) { if (SymbolTable == null) { if (index != 0) { throw new ArgumentException($"Cannot only add a symbol table at index 0", nameof(file)); } SymbolTable = symbolTable; } else { throw new ArgumentException($"Cannot add this symbol table as an existing symbol table is already present in the file entries of {this}", nameof(file)); } } else { if (SymbolTable != null && index == 0) { throw new ArgumentException($"Cannot add the entry {file} at index 0 because a symbol table is already set and must be the first entry in the list of files", nameof(file)); } } file.Index = (uint)index; _files.Insert(index, file); file.Parent = this; // Update the index of following files for (int i = index + 1; i < _files.Count; i++) { var nextFile = _files[i]; nextFile.Index++; } }
private void WriteFileEntry(Span <byte> buffer, ArFile file) { Debug.Assert((ulong)(Stream.Position - _startStreamOffset) == file.Offset); buffer.Fill((byte)' '); var name = file.InternalName; bool postFixSlash = false; if (name == null) { name = file.Name; if (ArArchiveFile.Kind != ArArchiveKind.Common && !name.EndsWith("/")) { postFixSlash = true; } } uint?bsdNameLength = null; if (ArArchiveFile.Kind == ArArchiveKind.BSD) { var nameLength = Encoding.UTF8.GetByteCount(name); if (nameLength > ArFile.FieldNameLength) { name = $"#1/{nameLength}"; bsdNameLength = (uint)nameLength; postFixSlash = false; } } // Encode Length int length = Encoding.UTF8.GetBytes(name, buffer.Slice(0, ArFile.FieldNameLength)); if (postFixSlash) { buffer[length] = (byte)'/'; } if (!(file is ArLongNamesTable)) { // 16 12 File modification timestamp Decimal EncodeDecimal(buffer, ArFile.FieldTimestampOffset, ArFile.FieldTimestampLength, (ulong)file.Timestamp.ToUnixTimeSeconds()); // 28 6 Owner ID Decimal EncodeDecimal(buffer, ArFile.FieldOwnerIdOffset, ArFile.FieldOwnerIdLength, file.OwnerId); // 34 6 Group ID Decimal EncodeDecimal(buffer, ArFile.FieldGroupIdOffset, ArFile.FieldGroupIdLength, file.GroupId); // 40 8 File mode Octal EncodeOctal(buffer, ArFile.FieldFileModeOffset, ArFile.FieldFileModeLength, file.FileMode); } // 48 10 File size in bytes Decimal EncodeDecimal(buffer, ArFile.FieldFileSizeOffset, ArFile.FieldFileSizeLength, file.Size); buffer[ArFile.FieldEndCharactersOffset] = 0x60; buffer[ArFile.FieldEndCharactersOffset + 1] = (byte)'\n'; // Write the entry Stream.Write(buffer); // Handle BSD file name by serializing the name before the data if it is required if (bsdNameLength.HasValue) { uint nameLength = bsdNameLength.Value; var bufferName = ArrayPool <byte> .Shared.Rent((int)nameLength); Encoding.UTF8.GetBytes(file.Name, 0, file.Name.Length, bufferName, 0); try { Stream.Write(bufferName, 0, (int)nameLength); } finally { ArrayPool <byte> .Shared.Return(bufferName); } } // Write the content following the entry file.WriteInternal(this); // Align to even byte if ((Stream.Position & 1) != 0) { Stream.WriteByte((byte)'\n'); } }
private bool TryReadFileEntry(Span <byte> buffer, out ArFile entry) { entry = null; Debug.Assert((Stream.Position & 1) == 0); long entryOffset = Stream.Position; int length = Stream.Read(buffer); if (length == 0) { return(false); } if (length < buffer.Length) { Diagnostics.Error(DiagnosticId.AR_ERR_InvalidFileEntryLength, $"Invalid length {length} while trying to read a file entry from stream at offset {entryOffset}. Expecting {buffer.Length} bytes"); return(false); } // 0 16 File identifier ASCII // discard right padding characters int idLength = 16; while (idLength > 0) { if (buffer[idLength - 1] != ' ') { break; } idLength--; } string name = null; ulong? bsdNameLength = null; if (idLength > 3 && ArArchiveFile.Kind == ArArchiveKind.BSD) { if (buffer[0] == '#' && buffer[1] == '1' && buffer[2] == '/') { // If we have a future header table, we are using it and expecting only numbers if (!TryDecodeDecimal(entryOffset, buffer, 3, ArFile.FieldNameLength - 3, $"BSD Name length following #1/ at offset {entryOffset}", out ulong bsdNameLengthDecoded)) { // Don't try to process more entries, the archive might be corrupted return(false); } bsdNameLength = bsdNameLengthDecoded; } } // If the last char is `/` // Keep file names with / or // // But remove trailing `/`for regular file names if (!bsdNameLength.HasValue && ArArchiveFile.Kind != ArArchiveKind.Common && idLength > 0 && buffer[idLength - 1] == '/') { if (!(idLength == 1 || idLength == 2 && buffer[idLength - 2] == '/')) { idLength--; } } if (_futureHeaders != null && buffer[0] == (byte)'/') { // If we have a future header table, we are using it and expecting only numbers if (!TryDecodeDecimal(entryOffset, buffer, 1, ArFile.FieldNameLength - 1, $"Name with offset to Future Headers Table at file offset {entryOffset}", out ulong offsetInFutureHeaders)) { // Don't try to process more entries, the archive might be corrupted return(false); } // If the number is ok, check that we have actually a string for this offset if (!_futureHeaders.FileNames.TryGetValue((int)offsetInFutureHeaders, out name)) { Diagnostics.Error(DiagnosticId.AR_ERR_InvalidReferenceToFutureHeadersTable, $"Invalid reference {offsetInFutureHeaders} found at file offset {entryOffset}. This file is invalid."); } } if (!bsdNameLength.HasValue && name == null) { name = idLength == 0 ? string.Empty : Encoding.UTF8.GetString(buffer.Slice(0, idLength)); } // 16 12 File modification timestamp Decimal if (!TryDecodeDecimal(entryOffset, buffer, ArFile.FieldTimestampOffset, ArFile.FieldTimestampLength, "File modification timestamp Decimal", out ulong timestamp)) { return(false); } // 28 6 Owner ID Decimal if (!TryDecodeDecimal(entryOffset, buffer, ArFile.FieldOwnerIdOffset, ArFile.FieldOwnerIdLength, "Owner ID", out ulong ownerId)) { return(false); } // 34 6 Group ID Decimal if (!TryDecodeDecimal(entryOffset, buffer, ArFile.FieldGroupIdOffset, ArFile.FieldGroupIdLength, "Group ID", out ulong groupId)) { return(false); } // 40 8 File mode Octal if (!TryDecodeOctal(entryOffset, buffer, ArFile.FieldFileModeOffset, ArFile.FieldFileModeLength, "File mode", out uint fileMode)) { return(false); } // 48 10 File size in bytes Decimal if (!TryDecodeDecimal(entryOffset, buffer, ArFile.FieldFileSizeOffset, ArFile.FieldFileSizeLength, "File size in bytes", out ulong fileSize)) { return(false); } // 58 2 Ending characters 0x60 0x0A if (buffer[ArFile.FieldEndCharactersOffset] != 0x60 || buffer[ArFile.FieldEndCharactersOffset + 1] != '\n') { Diagnostics.Error(DiagnosticId.AR_ERR_InvalidCharacterFoundInFileEntry, $"Invalid ASCII characters found 0x{buffer[ArFile.FieldEndCharactersOffset]:x} 0x{buffer[ArFile.FieldEndCharactersOffset+1]:x} instead of `\\n at the end of file entry at offset {entryOffset + ArFile.FieldEndCharactersOffset}"); return(false); } entry = CreateFileEntryFromName(name); entry.Timestamp = DateTimeOffset.FromUnixTimeSeconds((long)timestamp); entry.OwnerId = (uint)ownerId; entry.GroupId = (uint)groupId; entry.FileMode = fileMode; entry.Offset = (ulong)entryOffset; entry.Size = fileSize; // Read the BSD name if necessary if (bsdNameLength.HasValue) { var nameLength = (int)bsdNameLength.Value; var bufferForName = ArrayPool <byte> .Shared.Rent(nameLength); var streamPosition = Stream.Position; var dataReadCount = Stream.Read(bufferForName, 0, nameLength); if (dataReadCount != nameLength) { Diagnostics.Error(DiagnosticId.AR_ERR_UnexpectedEndOfFile, $"Unexpected end of file while trying to read the filename from the data section of the file entry at offset of {streamPosition}. Expecting {nameLength} bytes while only {dataReadCount} bytes were read from the stream."); return(false); } name = Encoding.UTF8.GetString(bufferForName, 0, nameLength); } if (!entry.IsSystem) { if (name.Contains('/')) { Diagnostics.Error(DiagnosticId.AR_ERR_InvalidCharacterInFileEntryName, $"The character `/` was found in the entry `{name}` while it is invalid."); return(false); } entry.Name = name; } entry.ReadInternal(this); // The end of an entry is always aligned if ((Stream.Position & 1) != 0) { long padOffset = Stream.Position; int pad = Stream.ReadByte(); if (pad < 0) { Diagnostics.Error(DiagnosticId.AR_ERR_UnexpectedEndOfFile, $"Unexpected end of file while trying to Invalid character 0x{pad:x} found at offset {padOffset} while expecting \\n 0xa"); return(false); } if (pad != '\n') { Diagnostics.Error(DiagnosticId.AR_ERR_ExpectingNewLineCharacter, $"Invalid character 0x{pad:x} found at offset {padOffset} while expecting \\n 0xa"); return(false); } } return(true); }