259 lines
12 KiB
Swift
259 lines
12 KiB
Swift
import Foundation
|
|
import WebKit
|
|
|
|
class IndexItem: Encodable {
|
|
let name: String
|
|
var dates = NSMutableOrderedSet()
|
|
var offsets = [UInt64]()
|
|
init(_ name: String) {
|
|
self.name = name
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(name, forKey: .name)
|
|
try container.encode(dates.array as! [UInt16], forKey: .dates)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case name
|
|
case dates
|
|
}
|
|
}
|
|
|
|
class Logs: NSObject, WKScriptMessageHandler {
|
|
let fm = FileManager.default;
|
|
let baseDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
|
var buffer = UnsafeMutableRawPointer.allocate(byteCount: 51000, alignment: 1)
|
|
var logDir: URL!
|
|
var character: String?
|
|
var index: [String: IndexItem]!
|
|
var loadedIndex: [String: IndexItem]!
|
|
|
|
func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
let data = message.body as! [String: AnyObject]
|
|
let key = data["_id"] as! String
|
|
do {
|
|
var result: String?
|
|
switch(data["_type"] as! String) {
|
|
case "init":
|
|
result = try initCharacter(data["character"] as! String)
|
|
case "loadIndex":
|
|
result = try loadIndex(data["character"] as! String)
|
|
case "getCharacters":
|
|
result = try getCharacters()
|
|
case "logMessage":
|
|
try logMessage(data["key"] as! String, data["conversation"] as! NSString, (data["time"] as! NSNumber).uint32Value, (data["type"] as! NSNumber).uint8Value, data["sender"] as! NSString, data["message"] as! NSString)
|
|
case "getBacklog":
|
|
result = try getBacklog(data["key"] as! String)
|
|
case "getLogs":
|
|
result = try getLogs(data["character"] as! String, data["key"] as! String, (data["date"] as! NSNumber).uint16Value)
|
|
case "repair":
|
|
try repair(data["character"] as! String)
|
|
default:
|
|
message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Unknown message type'))")
|
|
return
|
|
}
|
|
let output = result == nil ? "undefined" : result!;
|
|
message.webView!.evaluateJavaScript("nativeMessage('\(key)',\(output))")
|
|
} catch(let error) {
|
|
message.webView!.evaluateJavaScript("nativeError('\(key)',new Error('Logs-\(data["_type"]!): \(error.localizedDescription)'))")
|
|
}
|
|
}
|
|
|
|
func getIndex(_ character: String) throws -> [String: IndexItem] {
|
|
var index = [String: IndexItem]()
|
|
let files = try fm.contentsOfDirectory(at: baseDir.appendingPathComponent("\(character)/logs", isDirectory: true), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
|
|
for file in files {
|
|
if(!file.lastPathComponent.hasSuffix(".idx")) { continue }
|
|
let data = NSData(contentsOf: file)!
|
|
var nameLength = 0
|
|
data.getBytes(&nameLength, length: 1)
|
|
let name = String(data: data.subdata(with: NSMakeRange(1, nameLength)), encoding: .utf8)!
|
|
var offset = nameLength + 1
|
|
let indexItem = IndexItem(name)
|
|
if (data.length - offset) % 7 != 0 { throw NSError(domain: "Log corruption", code: 0) }
|
|
while offset < data.length {
|
|
var date: UInt16 = 0
|
|
data.getBytes(&date, range: NSMakeRange(offset, 2))
|
|
var o: UInt64 = 0
|
|
data.getBytes(&o, range: NSMakeRange(offset + 2, 5))
|
|
indexItem.dates.add(date)
|
|
indexItem.offsets.append(o)
|
|
offset += 7
|
|
}
|
|
index[file.deletingPathExtension().lastPathComponent] = indexItem
|
|
}
|
|
return index
|
|
}
|
|
|
|
func initCharacter(_ name: String) throws -> String {
|
|
logDir = baseDir.appendingPathComponent("\(name)/logs", isDirectory: true)
|
|
try fm.createDirectory(at: logDir, withIntermediateDirectories: true, attributes: nil)
|
|
index = try getIndex(name)
|
|
loadedIndex = index
|
|
character = name
|
|
return String(data: try JSONEncoder().encode(index), encoding: .utf8)!
|
|
}
|
|
|
|
func getCharacters() throws -> String {
|
|
let entries = try fm.contentsOfDirectory(at: baseDir, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles]).filter {
|
|
try $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory == true
|
|
}.map { $0.lastPathComponent }
|
|
return String(data: try JSONSerialization.data(withJSONObject: entries), encoding: .utf8)!;
|
|
}
|
|
|
|
func logMessage(_ key: String, _ conversation: NSString, _ time: UInt32, _ type: UInt8, _ sender: NSString, _ text: NSString) throws {
|
|
var time = time
|
|
var type = type
|
|
var day = UInt16(time / 86400)
|
|
let url = logDir.appendingPathComponent(key, isDirectory: false);
|
|
var indexItem = index![key]
|
|
if(indexItem == nil) { fm.createFile(atPath: url.path, contents: nil) }
|
|
let fd = try FileHandle(forWritingTo: url)
|
|
fd.seekToEndOfFile()
|
|
if(!(indexItem?.dates.contains(day) ?? false)) {
|
|
let indexFile = url.appendingPathExtension("idx")
|
|
if(indexItem == nil) { fm.createFile(atPath: indexFile.path, contents: nil) }
|
|
let indexFd = try FileHandle(forWritingTo: indexFile)
|
|
indexFd.seekToEndOfFile()
|
|
if(indexItem == nil) {
|
|
indexItem = IndexItem(conversation as String)
|
|
index![key] = indexItem
|
|
let cstring = conversation.utf8String
|
|
var length = strlen(cstring!)
|
|
write(indexFd.fileDescriptor, &length, 1)
|
|
write(indexFd.fileDescriptor, cstring, length)
|
|
}
|
|
write(indexFd.fileDescriptor, &day, 2)
|
|
var offset = fd.offsetInFile
|
|
write(indexFd.fileDescriptor, &offset, 5)
|
|
indexItem!.dates.add(day)
|
|
indexItem!.offsets.append(offset)
|
|
}
|
|
let start = fd.offsetInFile
|
|
write(fd.fileDescriptor, &time, 4)
|
|
write(fd.fileDescriptor, &type, 1)
|
|
var cstring = sender.utf8String
|
|
var length = strlen(cstring!)
|
|
write(fd.fileDescriptor, &length, 1)
|
|
write(fd.fileDescriptor, cstring, length)
|
|
cstring = text.utf8String
|
|
length = strlen(cstring!)
|
|
write(fd.fileDescriptor, &length, 2)
|
|
write(fd.fileDescriptor, cstring, length)
|
|
var size = fd.offsetInFile - start
|
|
write(fd.fileDescriptor, &size, 2)
|
|
}
|
|
|
|
func getBacklog(_ key: String) throws -> String {
|
|
let url = logDir.appendingPathComponent(key, isDirectory: false)
|
|
if(!fm.fileExists(atPath: url.path)) { return "[]" }
|
|
let file = try FileHandle(forReadingFrom: url)
|
|
file.seekToEndOfFile()
|
|
var strings = [String]()
|
|
strings.reserveCapacity(20)
|
|
while file.offsetInFile > 0 && strings.count < 20 {
|
|
file.seek(toFileOffset: file.offsetInFile - 2)
|
|
read(file.fileDescriptor, buffer, 2)
|
|
let length = buffer.load(as: UInt16.self)
|
|
if(length > file.offsetInFile - 2) { throw NSError(domain: "Log corruption", code: 0) }
|
|
let newOffset = file.offsetInFile - UInt64(length + 2)
|
|
file.seek(toFileOffset: newOffset)
|
|
read(file.fileDescriptor, buffer, Int(length))
|
|
strings.append(try deserializeMessage(buffer, 0).0)
|
|
file.seek(toFileOffset: newOffset)
|
|
}
|
|
return "[" + strings.reversed().joined(separator: ",") + "]"
|
|
}
|
|
|
|
func getLogs(_ character: String, _ key: String, _ date: UInt16) throws -> String {
|
|
let index = loadedIndex![key]
|
|
guard let indexKey = index?.dates.index(of: date) else { return "[]" }
|
|
let url = baseDir.appendingPathComponent("\(character)/logs/\(key)", isDirectory: false)
|
|
let file = try FileHandle(forReadingFrom: url)
|
|
let start = index!.offsets[indexKey]
|
|
let end = indexKey >= index!.offsets.count - 1 ? file.seekToEndOfFile() : index!.offsets[indexKey + 1]
|
|
file.seek(toFileOffset: start)
|
|
let length = Int(end - start)
|
|
let buffer = UnsafeMutableRawPointer.allocate(byteCount: length, alignment: 1)
|
|
read(file.fileDescriptor, buffer, length)
|
|
var json = "["
|
|
var offset = 0
|
|
while offset < length {
|
|
let deserialized = try deserializeMessage(buffer, offset)
|
|
offset = deserialized.1 + 2
|
|
json += deserialized.0 + ","
|
|
}
|
|
return json + "]"
|
|
}
|
|
|
|
func loadIndex(_ name: String) throws -> String {
|
|
loadedIndex = name == character ? index : try getIndex(name)
|
|
return String(data: try JSONEncoder().encode(loadedIndex), encoding: .utf8)!
|
|
}
|
|
|
|
func decodeString(_ buffer: UnsafeMutableRawPointer, _ offset: Int, _ length: Int) -> String? {
|
|
return String(bytesNoCopy: buffer.advanced(by: offset), length: length, encoding: .utf8, freeWhenDone: false)
|
|
}
|
|
|
|
func deserializeMessage(_ buffer: UnsafeMutableRawPointer, _ o: Int) throws -> (String, Int) {
|
|
var offset = o
|
|
let date = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee
|
|
let type = buffer.load(fromByteOffset: offset + 4, as: UInt8.self)
|
|
let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
|
|
guard let sender = decodeString(buffer, offset + 6, senderLength) else {
|
|
throw NSError(domain: "Log corruption", code: 0)
|
|
}
|
|
offset += senderLength + 6
|
|
let textLength = Int(buffer.advanced(by: offset).bindMemory(to: UInt16.self, capacity: 1).pointee)
|
|
guard let text = decodeString(buffer, offset + 2, textLength) else {
|
|
throw NSError(domain: "Log corruption", code: 0)
|
|
}
|
|
return ("{\"time\":\(date),\"type\":\(type),\"sender\":\(File.escape(sender)),\"text\":\(File.escape(text))}", offset + textLength + 2)
|
|
}
|
|
|
|
func repair(_ character: String) throws {
|
|
let files = try fm.contentsOfDirectory(at: baseDir.appendingPathComponent("\(character)/logs", isDirectory: true), includingPropertiesForKeys: nil, options: [.skipsHiddenFiles])
|
|
for file in files {
|
|
if(file.lastPathComponent.hasSuffix(".idx")) { continue }
|
|
let indexFd = try FileHandle(forUpdating: file.appendingPathExtension("idx"))
|
|
read(indexFd.fileDescriptor, buffer, 1)
|
|
indexFd.truncateFile(atOffset: UInt64(buffer.load(as: UInt8.self) + 1))
|
|
let fd = try FileHandle(forUpdating: file)
|
|
let size = fd.seekToEndOfFile()
|
|
fd.seek(toFileOffset: 0)
|
|
var lastDay = 0, pos = UInt64(0)
|
|
do {
|
|
while fd.offsetInFile < size {
|
|
pos = fd.offsetInFile
|
|
let max = read(fd.fileDescriptor, buffer, 51000)
|
|
var offset = 0
|
|
while offset + 10 < max {
|
|
let day = buffer.advanced(by: offset).bindMemory(to: UInt32.self, capacity: 1).pointee / 86400
|
|
let senderLength = Int(buffer.load(fromByteOffset: offset + 5, as: UInt8.self))
|
|
if offset + senderLength + 10 > max { break }
|
|
let sender = decodeString(buffer, offset + 6, senderLength)
|
|
let textLength = Int(buffer.advanced(by: offset + senderLength + 6).bindMemory(to: UInt16.self, capacity: 1).pointee)
|
|
if(offset + senderLength + textLength + 10 > max) { break }
|
|
let text = decodeString(buffer, offset + senderLength + 8, textLength)
|
|
let mark = senderLength + textLength + 8
|
|
let size = buffer.advanced(by: offset + mark).bindMemory(to: UInt16.self, capacity: 1).pointee
|
|
if(size != mark || sender == nil || text == nil) { throw NSError(domain: "", code: 0) }
|
|
if(day > lastDay) {
|
|
lastDay = Int(day)
|
|
write(indexFd.fileDescriptor, &lastDay, 2)
|
|
write(indexFd.fileDescriptor, &pos, 5)
|
|
}
|
|
offset = offset + mark + 2
|
|
pos = pos + UInt64(mark + 2)
|
|
}
|
|
if(offset == 0) { throw NSError(domain: "", code: 0) }
|
|
fd.seek(toFileOffset: pos)
|
|
}
|
|
} catch {
|
|
fd.truncateFile(atOffset: pos)
|
|
}
|
|
}
|
|
}
|
|
} |