// // WREpubPositionConverter.m // WeRead (读书) // Reverse-engineered implementation reconstruction // // Detailed pseudo-code based on binary analysis, ivar types, known methods, // and contextual knowledge of EPUB position/bookmark handling. // #import "WREpubPositionConverter.h" // --------------------------------------------------------------------------- // WRPositionPair // --------------------------------------------------------------------------- @implementation WRPositionPair - (instancetype)initWithRow:(NSInteger)row column:(NSInteger)column { self = [super init]; if (self) { _row = row; _column = column; } return self; } - (NSString *)description { return [NSString stringWithFormat:@"", (long)_row, (long)_column]; } @end // --------------------------------------------------------------------------- // WREpubPositionConverter // --------------------------------------------------------------------------- @implementation WREpubPositionConverter { // Core data from binary analysis NSArray *_filePaths; NSArray *_attributedStrings; NSMutableArray *_fileLengths; // character count per file NSMutableArray *_cumulativeOffsets; // prefix sum of character counts NSMutableDictionary *_indexCache; // cache: key -> index NSMutableDictionary *_stringRangeCache; // cache: key -> NSRange // Additional state inferred from context NSInteger _baseOffset; // offset for non-chapter content BOOL _containsIntroFlyleaf; // includes cover/flyleaf page BOOL _indicesBuilt; // whether initIndices has been called } #pragma mark - Lifecycle - (instancetype)initWithFilePaths:(NSArray *)filePaths attributedStrings:(NSArray *)attributedStrings offset:(NSInteger)offset isContainIntroFlyleaf:(BOOL)isContainIntroFlyleaf { self = [super init]; if (self) { _filePaths = [filePaths copy]; _attributedStrings = [attributedStrings copy]; _baseOffset = offset; _containsIntroFlyleaf = isContainIntroFlyleaf; _fileLengths = [NSMutableArray arrayWithCapacity:filePaths.count]; _cumulativeOffsets = [NSMutableArray arrayWithCapacity:filePaths.count]; _indexCache = [NSMutableDictionary dictionary]; _stringRangeCache = [NSMutableDictionary dictionary]; _indicesBuilt = NO; } return self; } #pragma mark - Index Building - (void)initIndices { if (_indicesBuilt) return; [_fileLengths removeAllObjects]; [_cumulativeOffsets removeAllObjects]; NSInteger cumulative = _baseOffset; for (NSUInteger i = 0; i < _attributedStrings.count; i++) { NSAttributedString *attrStr = _attributedStrings[i]; NSInteger length = (NSInteger)attrStr.string.length; [_fileLengths addObject:@(length)]; // Cumulative offset = sum of lengths of all previous files + baseOffset [_cumulativeOffsets addObject:@(cumulative)]; cumulative += length; } _indicesBuilt = YES; } #pragma mark - Position Conversion: Row/Column -> String Index - (void)indicesInFile:(NSString *)filePath forRowColumnPairs:(NSArray *)rowColumnPairs stringIndices:(NSArray *_Nullable *_Nullable)stringIndices string:(NSString *)string fileIndexOffset:(NSInteger)fileIndexOffset stringIndexOffset:(NSInteger)stringIndexOffset { // 1. Find the file index for the given path NSInteger fileIndex = [_filePaths indexOfObject:filePath]; if (fileIndex == NSNotFound) { // Try matching by last path component (filename only) NSString *filename = [filePath lastPathComponent]; for (NSUInteger i = 0; i < _filePaths.count; i++) { if ([[_filePaths[i] lastPathComponent] isEqualToString:filename]) { fileIndex = (NSInteger)i; break; } } } if (fileIndex == NSNotFound) { if (stringIndices) *stringIndices = @[]; return; } fileIndex += fileIndexOffset; // 2. Build a line-offset table from the string // // We need to map (row, column) -> character offset within the string. // A "row" is a line number; "column" is the character position in that line. // // Strategy: scan the string and record the starting offset of each line. NSMutableArray *lineStarts = [NSMutableArray array]; [lineStarts addObject:@(0)]; // line 0 starts at offset 0 NSUInteger len = string.length; for (NSUInteger i = 0; i < len; i++) { unichar c = [string characterAtIndex:i]; if (c == '\n' || c == '\r') { // Handle \r\n as a single line ending if (c == '\r' && i + 1 < len && [string characterAtIndex:i + 1] == '\n') { i++; // skip the \n } if (i + 1 < len) { [lineStarts addObject:@(i + 1)]; } } } // 3. Convert each (row, column) pair to a string index NSMutableArray *results = [NSMutableArray arrayWithCapacity:rowColumnPairs.count]; for (WRPositionPair *pair in rowColumnPairs) { NSInteger row = pair.row; NSInteger column = pair.column; if (row < 0 || row >= (NSInteger)lineStarts.count) { [results addObject:@(NSNotFound)]; continue; } NSInteger lineStart = [lineStarts[row] integerValue]; // Find the end of this line NSInteger lineEnd; if (row + 1 < (NSInteger)lineStarts.count) { lineEnd = [lineStarts[row + 1] integerValue] - 1; } else { lineEnd = (NSInteger)len; } NSInteger lineLength = lineEnd - lineStart; if (column > lineLength) { column = lineLength; // clamp to end of line } NSInteger stringIndex = lineStart + column + stringIndexOffset; // Also add the cumulative offset for this file to get the global position if (fileIndex >= 0 && fileIndex < (NSInteger)_cumulativeOffsets.count) { stringIndex += [_cumulativeOffsets[fileIndex] integerValue]; } [results addObject:@(stringIndex)]; } if (stringIndices) { *stringIndices = [results copy]; } } #pragma mark - Position Conversion: File Range -> String Range - (NSRange)stringRangeFromFileRange:(NSDictionary *)fileRange { // Expected keys in fileRange: // @"fileIndex" -> NSNumber (NSInteger) // @"start" -> NSNumber (NSInteger) local offset within the file // @"end" -> NSNumber (NSInteger) local offset within the file NSNumber *fileIndexNum = fileRange[@"fileIndex"]; NSNumber *startNum = fileRange[@"start"]; NSNumber *endNum = fileRange[@"end"]; if (!fileIndexNum || !startNum || !endNum) { return NSMakeRange(NSNotFound, 0); } NSInteger fileIndex = [fileIndexNum integerValue]; NSInteger localStart = [startNum integerValue]; NSInteger localEnd = [endNum integerValue]; // Check cache NSString *cacheKey = [NSString stringWithFormat:@"%ld:%ld:%ld", (long)fileIndex, (long)localStart, (long)localEnd]; NSNumber *cached = _stringRangeCache[cacheKey]; if (cached) { return NSRangeFromString(cached.stringValue); } // Validate file index if (!_indicesBuilt) [self initIndices]; if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) { return NSMakeRange(NSNotFound, 0); } // Get the cumulative offset for this file NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue]; // File-local length NSInteger fileLength = [_fileLengths[fileIndex] integerValue]; // Clamp to file bounds if (localStart < 0) localStart = 0; if (localEnd > fileLength) localEnd = fileLength; if (localStart >= localEnd) { return NSMakeRange(NSNotFound, 0); } NSInteger globalStart = cumulativeOffset + localStart; NSInteger length = localEnd - localStart; NSRange result = NSMakeRange(globalStart, length); // Cache the result _stringRangeCache[cacheKey] = NSStringFromRange(result); return result; } #pragma mark - Utility - (NSInteger)totalCharacterCount { if (!_indicesBuilt) [self initIndices]; if (_cumulativeOffsets.count == 0) return _baseOffset; NSInteger lastCumulative = [_cumulativeOffsets.lastObject integerValue]; NSInteger lastLength = [_fileLengths.lastObject integerValue]; return lastCumulative + lastLength; } - (NSInteger)fileIndexForCharacterPosition:(NSInteger)position { if (!_indicesBuilt) [self initIndices]; // Binary search through cumulative offsets NSInteger lo = 0; NSInteger hi = (NSInteger)_cumulativeOffsets.count - 1; NSInteger result = -1; while (lo <= hi) { NSInteger mid = lo + (hi - lo) / 2; NSInteger offset = [_cumulativeOffsets[mid] integerValue]; if (offset <= position) { result = mid; lo = mid + 1; } else { hi = mid - 1; } } // Verify the position is within this file's range if (result >= 0) { NSInteger fileLen = [_fileLengths[result] integerValue]; NSInteger cumOff = [_cumulativeOffsets[result] integerValue]; if (position >= cumOff + fileLen) { result = -1; // beyond end of last file } } return result; } - (NSInteger)localOffsetInFileAtIndex:(NSInteger)fileIndex forGlobalPosition:(NSInteger)position { if (!_indicesBuilt) [self initIndices]; if (fileIndex < 0 || fileIndex >= (NSInteger)_cumulativeOffsets.count) { return NSNotFound; } NSInteger cumulativeOffset = [_cumulativeOffsets[fileIndex] integerValue]; NSInteger localOffset = position - cumulativeOffset; NSInteger fileLen = [_fileLengths[fileIndex] integerValue]; if (localOffset < 0 || localOffset >= fileLen) { return NSNotFound; } return localOffset; } @end