/* * Copyright 2018 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.IO; using System.Text; using System.Web.Script.Serialization; using CommonUtil; namespace SourceGen { /// /// Load and save project data from/to a ".dis65" file. /// /// The various data structures get cloned to avoid situations where you can't freely /// rename and rearrange code because it's serialized directly to the save file. We /// want to provide a layer of indirection on fields, output enums as strings rather /// than digits, etc. /// /// Also, the JavaScriptSerializer can't deal with integer keys, so we have to convert /// dictionaries that use those to have string keys. /// /// On the deserialization side, we want to verify the inputs to avoid anything strange /// getting loaded that could cause a crash or weird behavior. The goal is to discard /// anything that looks wrong, providing a useful notification to the user, rather than /// failing outright. /// /// I'm expecting the save file format to expand and evolve over time, possibly in /// incompatible ways that require independent load routines for old and new formats. /// public static class ProjectFile { public const string FILENAME_EXT = ".dis65"; public static readonly string FILENAME_FILTER = Properties.Resources.FILE_FILTER_DIS65; // This is the version of content we're writing. Bump this any time we add anything. // This doesn't create forward or backward compatibility issues, because JSON will // ignore stuff that's in one side but not the other. However, if we're opening a // newer file in an older program, it's worth letting the user know that some stuff // may get lost as soon as they save the file. public const int CONTENT_VERSION = 1; private static bool ADD_CRLF = true; /// /// Serializes the project and writes it to the specified file. /// /// Project to serialize. /// Output path name. /// Human-readable error string, or an empty string if all /// went well. /// True on success. public static bool SerializeToFile(DisasmProject proj, string pathName, out string errorMessage) { try { string serializedData = SerializableProjectFile1.SerializeProject(proj); if (ADD_CRLF) { // Add some line breaks. This looks awful, but it makes text diffs // much more useful. serializedData = TextUtil.NonQuoteReplace(serializedData, "{", "{\r\n"); serializedData = TextUtil.NonQuoteReplace(serializedData, "},", "},\r\n"); } // Check to see if the project file is read-only. We want to fail early // so we don't leave our .TMP file sitting around -- the File.Delete() call // will fail if the destination is read-only. if (File.Exists(pathName) && (File.GetAttributes(pathName) & FileAttributes.ReadOnly) != 0) { throw new IOException(string.Format(Properties.Resources.ERR_FILE_READ_ONLY, pathName)); } // The BOM is not required or recommended for UTF-8 files, but anecdotal // evidence suggests that it's sometimes useful. Shouldn't cause any harm // to have it in the project file. The explicit Encoding.UTF8 argument // causes it to appear -- WriteAllText normally doesn't. // // Write to a temp file, then rename over original after write has succeeded. string tmpPath = pathName + ".TMP"; File.WriteAllText(tmpPath, serializedData, Encoding.UTF8); if (File.Exists(pathName)) { File.Delete(pathName); } File.Move(tmpPath, pathName); errorMessage = string.Empty; return true; } catch (Exception ex) { errorMessage = ex.Message; return false; } } /// /// Reads the specified file and deserializes it into the project. /// /// Input path name. /// Project to deserialize into. /// File load report, which may contain errors or warnings. /// True on success. public static bool DeserializeFromFile(string pathName, DisasmProject proj, out FileLoadReport report) { Debug.WriteLine("Deserializing '" + pathName + "'"); report = new FileLoadReport(pathName); string serializedData; try { serializedData = File.ReadAllText(pathName); } catch (Exception ex) { report.Add(FileLoadItem.Type.Error, Properties.Resources.ERR_PROJECT_LOAD_FAIL + ": " + ex.Message); return false; } if (serializedData.StartsWith(SerializableProjectFile1.MAGIC)) { // File is a match for SerializableProjectFile1. Strip header and deserialize. serializedData = serializedData.Substring(SerializableProjectFile1.MAGIC.Length); try { bool ok = SerializableProjectFile1.DeserializeProject(serializedData, proj, report); if (ok) { proj.UpdateCpuDef(); } return ok; } catch (Exception ex) { // Ideally this won't happen -- errors should be caught explicitly. This // is a catch-all to keep us from crashing on expectedly bad input. report.Add(FileLoadItem.Type.Error, Properties.Resources.ERR_PROJECT_FILE_CORRUPT + ": " + ex); return false; } } else { report.Add(FileLoadItem.Type.Error, Properties.Resources.ERR_NOT_PROJECT_FILE); return false; } } #if false public Dictionary IntKeysToStrings(Dictionary input) { Dictionary output = new Dictionary(); foreach (KeyValuePair entry in input) { output.Add(entry.Key.ToString(), entry.Value); } return output; } public Dictionary StringKeysToInts(Dictionary input) { Dictionary output = new Dictionary(); foreach (KeyValuePair entry in input) { if (!int.TryParse(entry.Key, out int intKey)) { throw new InvalidOperationException("bad non-int key: " + entry.Key); } output.Add(intKey, entry.Value); } return output; } #endif } /// /// Somewhat sloppy-looking JSON state dump. /// internal class SerializableProjectFile1 { // This appears at the top of the file, not as part of the JSON data. The version // number refers to the file format version, not the application version. public const string MAGIC = "### 6502bench SourceGen dis65 v1.0 ###"; public SerializableProjectFile1() { } public class SerProjectProperties { public string CpuName { get; set; } public bool IncludeUndocumentedInstr { get; set; } public int EntryFlags { get; set; } public SerAnalysisParameters AnalysisParams { get; set; } public List PlatformSymbolFileIdentifiers { get; set; } public List ExtensionScriptFileIdentifiers { get; set; } public SortedList ProjectSyms { get; set; } public SerProjectProperties() { } public SerProjectProperties(ProjectProperties props) { CpuName = Asm65.CpuDef.GetCpuNameFromType(props.CpuType); IncludeUndocumentedInstr = props.IncludeUndocumentedInstr; EntryFlags = props.EntryFlags.AsInt; AnalysisParams = new SerAnalysisParameters(props.AnalysisParams); // External file identifiers require no conversion. PlatformSymbolFileIdentifiers = props.PlatformSymbolFileIdentifiers; ExtensionScriptFileIdentifiers = props.ExtensionScriptFileIdentifiers; // Convert project-defined symbols to serializable form. ProjectSyms = new SortedList(); foreach (KeyValuePair kvp in props.ProjectSyms) { ProjectSyms.Add(kvp.Key, new SerDefSymbol(kvp.Value)); } } } public class SerAnalysisParameters { public bool AnalyzeUncategorizedData { get; set; } public int MinCharsForString { get; set; } public bool SeekNearbyTargets { get; set; } public SerAnalysisParameters() { } public SerAnalysisParameters(ProjectProperties.AnalysisParameters src) { AnalyzeUncategorizedData = src.AnalyzeUncategorizedData; MinCharsForString = src.MinCharsForString; SeekNearbyTargets = src.SeekNearbyTargets; } } public class SerAddressMap { public int Offset { get; set; } public int Addr { get; set; } // Length is computed field, no need to serialize public SerAddressMap() { } public SerAddressMap(AddressMap.AddressMapEntry ent) { Offset = ent.Offset; Addr = ent.Addr; } } public class SerTypeHintRange { public int Low { get; set; } public int High { get; set; } public string Hint { get; set; } public SerTypeHintRange() { } public SerTypeHintRange(int low, int high, string hintStr) { Low = low; High = high; Hint = hintStr; } } public class SerMultiLineComment { // NOTE: Text must be CRLF at line breaks. public string Text { get; set; } public bool BoxMode { get; set; } public int MaxWidth { get; set; } public int BackgroundColor { get; set; } public SerMultiLineComment() { } public SerMultiLineComment(MultiLineComment mlc) { Text = mlc.Text; BoxMode = mlc.BoxMode; MaxWidth = mlc.MaxWidth; BackgroundColor = mlc.BackgroundColor.ToArgb(); } } public class SerSymbol { public string Label { get; set; } public int Value { get; set; } public string Source { get; set; } public string Type { get; set; } public SerSymbol() { } public SerSymbol(Symbol sym) { Label = sym.Label; Value = sym.Value; Source = sym.SymbolSource.ToString(); Type = sym.SymbolType.ToString(); } } public class SerFormatDescriptor { public int Length { get; set; } public string Format { get; set; } public string SubFormat { get; set; } public SerWeakSymbolRef SymbolRef { get; set; } public SerFormatDescriptor() { } public SerFormatDescriptor(FormatDescriptor dfd) { Length = dfd.Length; Format = dfd.FormatType.ToString(); SubFormat = dfd.FormatSubType.ToString(); if (dfd.SymbolRef != null) { SymbolRef = new SerWeakSymbolRef(dfd.SymbolRef); } } } public class SerWeakSymbolRef { public string Label { get; set; } public string Part { get; set; } public SerWeakSymbolRef() { } public SerWeakSymbolRef(WeakSymbolRef weakSym) { Label = weakSym.Label; Part = weakSym.ValuePart.ToString(); } } public class SerDefSymbol : SerSymbol { public SerFormatDescriptor DataDescriptor { get; set; } public string Comment { get; set; } public SerDefSymbol() { } public SerDefSymbol(DefSymbol defSym) : base(defSym) { DataDescriptor = new SerFormatDescriptor(defSym.DataDescriptor); Comment = defSym.Comment; } } // Fields are serialized to/from JSON. Do not change the field names. public int _ContentVersion { get; set; } public int FileDataLength { get; set; } public int FileDataCrc32 { get; set; } public SerProjectProperties ProjectProps { get; set; } public List AddressMap { get; set; } public List TypeHints { get; set; } public Dictionary StatusFlagOverrides { get; set; } public Dictionary Comments { get; set; } public Dictionary LongComments { get; set; } public Dictionary Notes { get; set; } public Dictionary UserLabels { get; set; } public Dictionary OperandFormats { get; set; } /// /// Serializes a DisasmProject into an augmented JSON string. /// /// Project to serialize. /// Augmented JSON string. public static string SerializeProject(DisasmProject proj) { StringBuilder sb = new StringBuilder(); sb.Append(MAGIC); // augment with version string, which will be stripped sb.Append("\r\n"); // will be ignored by deserializer; might get converted to \n SerializableProjectFile1 spf = new SerializableProjectFile1(); spf._ContentVersion = ProjectFile.CONTENT_VERSION; Debug.Assert(proj.FileDataLength == proj.FileData.Length); spf.FileDataLength = proj.FileDataLength; spf.FileDataCrc32 = (int)proj.FileDataCrc32; // Convert AddressMap to serializable form. spf.AddressMap = new List(); foreach (AddressMap.AddressMapEntry ent in proj.AddrMap) { spf.AddressMap.Add(new SerAddressMap(ent)); } // Reduce TypeHints to a collection of ranges. Output the type enum as a string // so we're not tied to a specific value. spf.TypeHints = new List(); TypedRangeSet trs = new TypedRangeSet(); for (int i = 0; i < proj.TypeHints.Length; i++) { trs.Add(i, (int)proj.TypeHints[i]); } IEnumerator iter = trs.RangeListIterator; while (iter.MoveNext()) { if (iter.Current.Type == (int)CodeAnalysis.TypeHint.NoHint) { continue; } spf.TypeHints.Add(new SerTypeHintRange(iter.Current.Low, iter.Current.High, ((CodeAnalysis.TypeHint)iter.Current.Type).ToString())); } // Convert StatusFlagOverrides to serializable form. Just write the state out // as an integer... not expecting it to change. If it does, we can convert. spf.StatusFlagOverrides = new Dictionary(); for (int i = 0; i < proj.StatusFlagOverrides.Length; i++) { if (proj.StatusFlagOverrides[i] == Asm65.StatusFlags.DefaultValue) { continue; } spf.StatusFlagOverrides.Add(i.ToString(), proj.StatusFlagOverrides[i].AsInt); } // Convert Comments to serializable form. spf.Comments = new Dictionary(); for (int i = 0; i < proj.Comments.Length; i++) { if (string.IsNullOrEmpty(proj.Comments[i])) { continue; } spf.Comments.Add(i.ToString(), proj.Comments[i]); } // Convert multi-line comments to serializable form. spf.LongComments = new Dictionary(); foreach (KeyValuePair kvp in proj.LongComments) { spf.LongComments.Add(kvp.Key.ToString(), new SerMultiLineComment(kvp.Value)); } // Convert multi-line notes to serializable form. spf.Notes = new Dictionary(); foreach (KeyValuePair kvp in proj.Notes) { spf.Notes.Add(kvp.Key.ToString(), new SerMultiLineComment(kvp.Value)); } // Convert user-defined labels to serializable form. spf.UserLabels = new Dictionary(); foreach (KeyValuePair kvp in proj.UserLabels) { spf.UserLabels.Add(kvp.Key.ToString(), new SerSymbol(kvp.Value)); } // Convert operand and data item format descriptors to serializable form. spf.OperandFormats = new Dictionary(); foreach (KeyValuePair kvp in proj.OperandFormats) { spf.OperandFormats.Add(kvp.Key.ToString(), new SerFormatDescriptor(kvp.Value)); } spf.ProjectProps = new SerProjectProperties(proj.ProjectProps); JavaScriptSerializer ser = new JavaScriptSerializer(); string cereal = ser.Serialize(spf); sb.Append(cereal); // Stick a linefeed at the end. Makes git happier. sb.Append("\r\n"); return sb.ToString(); } /// /// Deserializes an augmented JSON string into a DisasmProject. /// /// Serialized data. /// Project to populate. /// Error report object. /// True on success, false on fatal error. public static bool DeserializeProject(string cereal, DisasmProject proj, FileLoadReport report) { JavaScriptSerializer ser = new JavaScriptSerializer(); SerializableProjectFile1 spf; try { spf = ser.Deserialize(cereal); } catch (Exception ex) { // The deserializer seems to be stuffing the entire data stream into the // exception, which we don't really want, so cap it at 256 chars. string msg = ex.Message; if (msg.Length > 256) { msg = msg.Substring(0, 256) + " [...]"; } report.Add(FileLoadItem.Type.Error, Properties.Resources.ERR_PROJECT_FILE_CORRUPT + ": " + msg); return false; } if (spf._ContentVersion > ProjectFile.CONTENT_VERSION) { // Post a warning. report.Add(FileLoadItem.Type.Notice, Properties.Resources.PROJECT_FROM_NEWER_APP); } if (spf.FileDataLength <= 0) { report.Add(FileLoadItem.Type.Error, Properties.Resources.ERR_BAD_FILE_LENGTH + ": " + spf.FileDataLength); return false; } // Initialize the object and set a few simple items. proj.Initialize(spf.FileDataLength); proj.SetFileCrc((uint)spf.FileDataCrc32); // Deserialize ProjectProperties: misc items. proj.ProjectProps.CpuType = Asm65.CpuDef.GetCpuTypeFromName(spf.ProjectProps.CpuName); proj.ProjectProps.IncludeUndocumentedInstr = spf.ProjectProps.IncludeUndocumentedInstr; proj.ProjectProps.EntryFlags = Asm65.StatusFlags.FromInt(spf.ProjectProps.EntryFlags); proj.ProjectProps.AnalysisParams = new ProjectProperties.AnalysisParameters(); proj.ProjectProps.AnalysisParams.AnalyzeUncategorizedData = spf.ProjectProps.AnalysisParams.AnalyzeUncategorizedData; proj.ProjectProps.AnalysisParams.MinCharsForString = spf.ProjectProps.AnalysisParams.MinCharsForString; proj.ProjectProps.AnalysisParams.SeekNearbyTargets = spf.ProjectProps.AnalysisParams.SeekNearbyTargets; // Deserialize ProjectProperties: external file identifiers. Debug.Assert(proj.ProjectProps.PlatformSymbolFileIdentifiers.Count == 0); foreach (string str in spf.ProjectProps.PlatformSymbolFileIdentifiers) { proj.ProjectProps.PlatformSymbolFileIdentifiers.Add(str); } Debug.Assert(proj.ProjectProps.ExtensionScriptFileIdentifiers.Count == 0); foreach (string str in spf.ProjectProps.ExtensionScriptFileIdentifiers) { proj.ProjectProps.ExtensionScriptFileIdentifiers.Add(str); } // Deserialize ProjectProperties: project symbols. foreach (KeyValuePair kvp in spf.ProjectProps.ProjectSyms) { if (!CreateSymbol(kvp.Value, report, out Symbol sym)) { continue; } if (!CreateFormatDescriptor(kvp.Value.DataDescriptor, report, out FormatDescriptor dfd)) { continue; } proj.ProjectProps.ProjectSyms[sym.Label] = new DefSymbol(sym, dfd, kvp.Value.Comment); } // Deserialize address map. foreach (SerAddressMap addr in spf.AddressMap) { proj.AddrMap.Set(addr.Offset, addr.Addr); } // Deserialize type hints. Default value in new array as NoHint, so we don't // need to write those. They should not have been recorded in the file. foreach (SerTypeHintRange range in spf.TypeHints) { if (range.Low < 0 || range.High >= spf.FileDataLength || range.Low > range.High) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_RANGE + ": " + Properties.Resources.PROJECT_FIELD_TYPE_HINT + " +" + range.Low.ToString("x6") + " - +" + range.High.ToString("x6")); continue; } CodeAnalysis.TypeHint hint; try { hint = (CodeAnalysis.TypeHint) Enum.Parse( typeof(CodeAnalysis.TypeHint), range.Hint); } catch (ArgumentException) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_TYPE_HINT + ": " + range.Hint); continue; } for (int i = range.Low; i <= range.High; i++) { proj.TypeHints[i] = hint; } } // Deserialize status flag overrides. foreach (KeyValuePair kvp in spf.StatusFlagOverrides) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_STATUS_FLAGS, report, out int intKey)) { continue; } proj.StatusFlagOverrides[intKey] = Asm65.StatusFlags.FromInt(kvp.Value); } // Deserialize comments. foreach (KeyValuePair kvp in spf.Comments) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_COMMENT, report, out int intKey)) { continue; } proj.Comments[intKey] = kvp.Value; } // Deserialize long comments. foreach (KeyValuePair kvp in spf.LongComments) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_LONG_COMMENT, report, out int intKey)) { continue; } proj.LongComments[intKey] = new MultiLineComment(kvp.Value.Text, kvp.Value.BoxMode, kvp.Value.MaxWidth); } // Deserialize notes. foreach (KeyValuePair kvp in spf.Notes) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_NOTE, report, out int intKey)) { continue; } proj.Notes[intKey] = new MultiLineComment(kvp.Value.Text, Color.FromArgb(kvp.Value.BackgroundColor)); } // Deserialize user-defined labels. SortedList labelDupCheck = new SortedList(spf.UserLabels.Count); foreach (KeyValuePair kvp in spf.UserLabels) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_USER_LABEL, report, out int intKey)) { continue; } Symbol.Source source; Symbol.Type type; try { source = (Symbol.Source)Enum.Parse(typeof(Symbol.Source), kvp.Value.Source); type = (Symbol.Type)Enum.Parse(typeof(Symbol.Type), kvp.Value.Type); if (source != Symbol.Source.User) { // User labels are always source=user. I don't think it really matters, // but best to keep junk out. throw new Exception("wrong source for user label"); } } catch (ArgumentException) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_SYMBOL_ST + ": " + kvp.Value.Source + "/" + kvp.Value.Type); continue; } // Check for duplicate labels. We only want to compare label strings, so we // can't test UserLabels.ContainsValue (which might be a linear search anyway). // Dump the labels into a sorted list. if (labelDupCheck.ContainsKey(kvp.Value.Label)) { report.Add(FileLoadItem.Type.Warning, string.Format(Properties.Resources.ERR_DUPLICATE_LABEL, kvp.Value.Label, intKey)); continue; } labelDupCheck.Add(kvp.Value.Label, string.Empty); proj.UserLabels[intKey] = new Symbol(kvp.Value.Label, kvp.Value.Value, source, type); } // Deserialize operand format descriptors. foreach (KeyValuePair kvp in spf.OperandFormats) { if (!ParseValidateKey(kvp.Key, spf.FileDataLength, Properties.Resources.PROJECT_FIELD_OPERAND_FORMAT, report, out int intKey)) { continue; } if (!CreateFormatDescriptor(kvp.Value, report, out FormatDescriptor dfd)) { continue; } if (intKey < 0 || intKey + dfd.Length > spf.FileDataLength) { report.Add(FileLoadItem.Type.Warning, string.Format(Properties.Resources.ERR_BAD_FD, intKey)); continue; } // TODO(maybe): check to see if the descriptor straddles an address change. // Not fatal but it'll make things look weird. proj.OperandFormats[intKey] = dfd; } return true; } /// /// Creates a Symbol from a SerSymbol. /// /// Deserialized data. /// Error report object. /// /// True on success. private static bool CreateSymbol(SerSymbol ssym, FileLoadReport report, out Symbol outSym) { outSym = null; Symbol.Source source; Symbol.Type type; try { source = (Symbol.Source)Enum.Parse(typeof(Symbol.Source), ssym.Source); type = (Symbol.Type)Enum.Parse(typeof(Symbol.Type), ssym.Type); } catch (ArgumentException) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_SYMBOL_ST + ": " + ssym.Source + "/" + ssym.Type); return false; } outSym = new Symbol(ssym.Label, ssym.Value, source, type/*, ssym.IsExport*/); return true; } /// /// Creates a FormatDescriptor from a SerFormatDescriptor. /// /// Deserialized data. /// Error report object. /// Created FormatDescriptor. /// True on success. private static bool CreateFormatDescriptor(SerFormatDescriptor sfd, FileLoadReport report, out FormatDescriptor dfd) { dfd = null; FormatDescriptor.Type format; FormatDescriptor.SubType subFormat; try { format = (FormatDescriptor.Type)Enum.Parse( typeof(FormatDescriptor.Type), sfd.Format); subFormat = (FormatDescriptor.SubType)Enum.Parse( typeof(FormatDescriptor.SubType), sfd.SubFormat); } catch (ArgumentException) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_FD_FORMAT + ": " + sfd.Format + "/" + sfd.SubFormat); return false; } if (sfd.SymbolRef == null) { dfd = FormatDescriptor.Create(sfd.Length, format, subFormat); } else { WeakSymbolRef.Part part; try { part = (WeakSymbolRef.Part)Enum.Parse( typeof(WeakSymbolRef.Part), sfd.SymbolRef.Part); } catch (ArgumentException) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_BAD_SYMREF_PART + ": " + sfd.SymbolRef.Part); return false; } dfd = FormatDescriptor.Create(sfd.Length, new WeakSymbolRef(sfd.SymbolRef.Label, part), format == FormatDescriptor.Type.NumericBE); } return true; } /// /// Parses an integer key that was stored as a string, and checks to see if the /// value falls within an acceptable range. /// /// Integer key, in string form. /// Length of file, for range check. /// Name of field, for error messages. /// Error report object. /// Returned integer key. /// True on success, false on failure. private static bool ParseValidateKey(string keyStr, int fileLen, string fieldName, FileLoadReport report, out int intKey) { if (!int.TryParse(keyStr, out intKey)) { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_INVALID_INT_VALUE + " (" + fieldName + ": " + keyStr + ")"); return false; } // Shouldn't allow DisplayList.Line.HEADER_COMMENT_OFFSET on anything but // LongComment. Maybe "bool allowNegativeKeys"? if (intKey < fileLen && (intKey >= 0 || intKey == DisplayList.Line.HEADER_COMMENT_OFFSET)) { return true; } else { report.Add(FileLoadItem.Type.Warning, Properties.Resources.ERR_INVALID_KEY_VALUE + " (" + fieldName + ": " + intKey + ")"); return false; } } } }