327 lines
10 KiB
Objective-C
327 lines
10 KiB
Objective-C
//
|
|
// 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:@"<WRPositionPair row=%ld col=%ld>",
|
|
(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<NSString *> *)filePaths
|
|
attributedStrings:(NSArray<NSAttributedString *> *)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<WRPositionPair *> *)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<NSNumber *> *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<NSNumber *> *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
|