expo-tiddlywiki-filesystem-android-external-storage 2.2.3 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -13,11 +13,15 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|
|
13
13
|
import org.json.JSONArray
|
|
14
14
|
import org.json.JSONObject
|
|
15
15
|
import java.io.BufferedInputStream
|
|
16
|
+
import java.io.ByteArrayOutputStream
|
|
16
17
|
import java.io.File
|
|
17
18
|
import java.io.FileInputStream
|
|
18
19
|
import java.io.FileOutputStream
|
|
19
20
|
import java.io.RandomAccessFile
|
|
21
|
+
import java.nio.ByteBuffer
|
|
22
|
+
import java.security.MessageDigest
|
|
20
23
|
import java.util.concurrent.TimeUnit
|
|
24
|
+
import java.util.zip.InflaterInputStream
|
|
21
25
|
|
|
22
26
|
/**
|
|
23
27
|
* Expo native module that performs raw java.io.File I/O on external storage.
|
|
@@ -741,6 +745,93 @@ class ExternalStorageModule : Module() {
|
|
|
741
745
|
result.toString()
|
|
742
746
|
}
|
|
743
747
|
|
|
748
|
+
/**
|
|
749
|
+
* Build a .git/index file from scratch by:
|
|
750
|
+
* 1. Resolving HEAD → commit SHA → tree SHA
|
|
751
|
+
* 2. Walking the tree recursively to enumerate (path, mode, blobSHA)
|
|
752
|
+
* 3. Stat'ing each file on disk natively
|
|
753
|
+
* 4. Writing a v2 binary .git/index file
|
|
754
|
+
*
|
|
755
|
+
* This is MUCH faster than isomorphic-git checkout because everything
|
|
756
|
+
* runs natively without JS↔Kotlin bridge per-file overhead.
|
|
757
|
+
*
|
|
758
|
+
* @param gitRootDir The repo root (parent of .git/)
|
|
759
|
+
* @return JSON string with status: {"ok":true,"entries":N} or {"ok":false,"error":"..."}
|
|
760
|
+
*/
|
|
761
|
+
AsyncFunction("buildGitIndex") { gitRootDir: String ->
|
|
762
|
+
val root = File(gitRootDir)
|
|
763
|
+
val gitDir = File(root, ".git")
|
|
764
|
+
if (!gitDir.isDirectory) throw Exception("Not a git repository: $gitRootDir")
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
// 1. Resolve HEAD to a commit SHA
|
|
768
|
+
val headFile = File(gitDir, "HEAD")
|
|
769
|
+
val headContent = headFile.readText(Charsets.UTF_8).trim()
|
|
770
|
+
val commitSha: String = if (headContent.startsWith("ref: ")) {
|
|
771
|
+
val refPath = headContent.removePrefix("ref: ")
|
|
772
|
+
val refFile = File(gitDir, refPath)
|
|
773
|
+
if (refFile.exists()) {
|
|
774
|
+
refFile.readText(Charsets.UTF_8).trim()
|
|
775
|
+
} else {
|
|
776
|
+
// Try packed-refs
|
|
777
|
+
resolvePackedRef(gitDir, refPath)
|
|
778
|
+
?: throw Exception("Cannot resolve HEAD ref: $refPath")
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
headContent // Detached HEAD — already a SHA
|
|
782
|
+
}
|
|
783
|
+
android.util.Log.i("BuildGitIndex", "HEAD commit: $commitSha")
|
|
784
|
+
|
|
785
|
+
// 2. Read the commit object → get tree SHA
|
|
786
|
+
val commitBytes = readGitObject(gitDir, commitSha)
|
|
787
|
+
?: throw Exception("Cannot read commit object: $commitSha")
|
|
788
|
+
val treeSha = parseCommitTreeSha(commitBytes)
|
|
789
|
+
?: throw Exception("Cannot find tree SHA in commit: $commitSha")
|
|
790
|
+
android.util.Log.i("BuildGitIndex", "Root tree: $treeSha")
|
|
791
|
+
|
|
792
|
+
// 3. Walk the tree recursively → collect all entries
|
|
793
|
+
val entries = mutableListOf<GitTreeEntry>()
|
|
794
|
+
fun walkTree(sha: String, prefix: String) {
|
|
795
|
+
val treeBytes = readGitObject(gitDir, sha)
|
|
796
|
+
?: throw Exception("Cannot read tree object: $sha")
|
|
797
|
+
parseTreeEntries(treeBytes).forEach { (name, entryMode, entrySha) ->
|
|
798
|
+
val fullPath = if (prefix.isEmpty()) name else "$prefix/$name"
|
|
799
|
+
if (entryMode == 0x4000 || entryMode == 0o40000) {
|
|
800
|
+
// Directory — recurse
|
|
801
|
+
walkTree(bytesToHex(entrySha), fullPath)
|
|
802
|
+
} else {
|
|
803
|
+
entries.add(GitTreeEntry(fullPath, entryMode, entrySha))
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
walkTree(treeSha, "")
|
|
808
|
+
android.util.Log.i("BuildGitIndex", "Tree walk found ${entries.size} entries")
|
|
809
|
+
|
|
810
|
+
// 4. Sort entries by path (git index requires sorted order)
|
|
811
|
+
entries.sortBy { it.path }
|
|
812
|
+
|
|
813
|
+
// 5. Build the binary index
|
|
814
|
+
val indexBytes = buildIndexBinary(root, entries)
|
|
815
|
+
|
|
816
|
+
// 6. Write to .git/index
|
|
817
|
+
val indexFile = File(gitDir, "index")
|
|
818
|
+
indexFile.writeBytes(indexBytes)
|
|
819
|
+
android.util.Log.i("BuildGitIndex", "Wrote index: ${indexBytes.size} bytes, ${entries.size} entries")
|
|
820
|
+
|
|
821
|
+
val result = JSONObject()
|
|
822
|
+
result.put("ok", true)
|
|
823
|
+
result.put("entries", entries.size)
|
|
824
|
+
result.put("indexSize", indexBytes.size)
|
|
825
|
+
result.toString()
|
|
826
|
+
} catch (e: Exception) {
|
|
827
|
+
android.util.Log.e("BuildGitIndex", "Failed: ${e.message}", e)
|
|
828
|
+
val result = JSONObject()
|
|
829
|
+
result.put("ok", false)
|
|
830
|
+
result.put("error", e.message ?: "Unknown error")
|
|
831
|
+
result.toString()
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
744
835
|
// ─── TiddlyWiki batch file parsing ─────────────────────────────────
|
|
745
836
|
|
|
746
837
|
/**
|
|
@@ -1061,6 +1152,409 @@ class ExternalStorageModule : Module() {
|
|
|
1061
1152
|
}
|
|
1062
1153
|
}
|
|
1063
1154
|
|
|
1155
|
+
// ─── Git byte-conversion helpers ─────────────────────────────────
|
|
1156
|
+
|
|
1157
|
+
/** Convert a byte array to a lowercase hex string */
|
|
1158
|
+
private fun bytesToHex(bytes: ByteArray): String {
|
|
1159
|
+
return bytes.joinToString("") { "%02x".format(it) }
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/** Convert a hex string to a byte array */
|
|
1163
|
+
private fun hexToBytes(hex: String): ByteArray {
|
|
1164
|
+
val len = hex.length
|
|
1165
|
+
val data = ByteArray(len / 2)
|
|
1166
|
+
var i = 0
|
|
1167
|
+
while (i < len) {
|
|
1168
|
+
data[i / 2] = ((Character.digit(hex[i], 16) shl 4) + Character.digit(hex[i + 1], 16)).toByte()
|
|
1169
|
+
i += 2
|
|
1170
|
+
}
|
|
1171
|
+
return data
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Compare two 20-byte SHA-1 arrays lexicographically (unsigned) */
|
|
1175
|
+
private fun compareSha(a: ByteArray, b: ByteArray): Int {
|
|
1176
|
+
for (i in a.indices) {
|
|
1177
|
+
val av = a[i].toInt() and 0xFF
|
|
1178
|
+
val bv = b[i].toInt() and 0xFF
|
|
1179
|
+
if (av != bv) return av - bv
|
|
1180
|
+
}
|
|
1181
|
+
return 0
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ─── Git object reading helpers ──────────────────────────────────
|
|
1185
|
+
|
|
1186
|
+
/** Resolve a ref from .git/packed-refs */
|
|
1187
|
+
private fun resolvePackedRef(gitDir: File, refPath: String): String? {
|
|
1188
|
+
val packedRefs = File(gitDir, "packed-refs")
|
|
1189
|
+
if (!packedRefs.exists()) return null
|
|
1190
|
+
for (line in packedRefs.readLines(Charsets.UTF_8)) {
|
|
1191
|
+
if (line.startsWith("#") || line.isBlank()) continue
|
|
1192
|
+
val parts = line.split(" ", limit = 2)
|
|
1193
|
+
if (parts.size == 2 && parts[1].trim() == refPath) {
|
|
1194
|
+
return parts[0].trim()
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
return null
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Read a git object by its SHA-1 hex string.
|
|
1202
|
+
* Tries loose objects first (.git/objects/ab/cdef...),
|
|
1203
|
+
* then falls back to pack files (.git/objects/pack/*.pack).
|
|
1204
|
+
* Returns the raw object content (after the "type size\0" header is stripped).
|
|
1205
|
+
*/
|
|
1206
|
+
private fun readGitObject(gitDir: File, sha: String): ByteArray? {
|
|
1207
|
+
// Try loose object first
|
|
1208
|
+
val looseFile = File(gitDir, "objects/${sha.substring(0, 2)}/${sha.substring(2)}")
|
|
1209
|
+
if (looseFile.exists()) {
|
|
1210
|
+
return readLooseObject(looseFile)
|
|
1211
|
+
}
|
|
1212
|
+
// Try pack files
|
|
1213
|
+
val packDir = File(gitDir, "objects/pack")
|
|
1214
|
+
if (!packDir.isDirectory) return null
|
|
1215
|
+
val idxFiles = packDir.listFiles { _, name -> name.endsWith(".idx") } ?: return null
|
|
1216
|
+
for (idxFile in idxFiles) {
|
|
1217
|
+
val packFile = File(idxFile.absolutePath.replace(".idx", ".pack"))
|
|
1218
|
+
if (!packFile.exists()) continue
|
|
1219
|
+
val offset = findObjectInPackIndex(idxFile, sha)
|
|
1220
|
+
if (offset != null) {
|
|
1221
|
+
return readObjectFromPack(packFile, offset, gitDir)
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return null
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** Read and decompress a loose git object, returning raw content after header. */
|
|
1228
|
+
private fun readLooseObject(file: File): ByteArray {
|
|
1229
|
+
val compressed = file.readBytes()
|
|
1230
|
+
val inflated = java.util.zip.Inflater().let { inflater ->
|
|
1231
|
+
inflater.setInput(compressed)
|
|
1232
|
+
val buf = ByteArray(8192)
|
|
1233
|
+
val baos = ByteArrayOutputStream()
|
|
1234
|
+
while (!inflater.finished()) {
|
|
1235
|
+
val n = inflater.inflate(buf)
|
|
1236
|
+
baos.write(buf, 0, n)
|
|
1237
|
+
}
|
|
1238
|
+
inflater.end()
|
|
1239
|
+
baos.toByteArray()
|
|
1240
|
+
}
|
|
1241
|
+
// Skip "type size\0" header
|
|
1242
|
+
val nullIdx = inflated.indexOf(0.toByte())
|
|
1243
|
+
return if (nullIdx >= 0) inflated.copyOfRange(nullIdx + 1, inflated.size) else inflated
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Find an object's offset in a pack index file (v2 format).
|
|
1248
|
+
* Returns the byte offset within the .pack file, or null if not found.
|
|
1249
|
+
*/
|
|
1250
|
+
private fun findObjectInPackIndex(idxFile: File, sha: String): Long? {
|
|
1251
|
+
val shaBytes = hexToBytes(sha)
|
|
1252
|
+
RandomAccessFile(idxFile, "r").use { raf ->
|
|
1253
|
+
// V2 index starts with 0xff744f63 magic + 4-byte version
|
|
1254
|
+
val magic = ByteArray(4)
|
|
1255
|
+
raf.readFully(magic)
|
|
1256
|
+
if (magic[0] != 0xFF.toByte() || magic[1] != 0x74.toByte() ||
|
|
1257
|
+
magic[2] != 0x4F.toByte() || magic[3] != 0x63.toByte()) {
|
|
1258
|
+
return null // Not a v2 index
|
|
1259
|
+
}
|
|
1260
|
+
raf.readInt() // version (should be 2)
|
|
1261
|
+
|
|
1262
|
+
// Fanout table: 256 entries of 4-byte big-endian counts
|
|
1263
|
+
val fanout = IntArray(256)
|
|
1264
|
+
for (i in 0 until 256) {
|
|
1265
|
+
fanout[i] = raf.readInt()
|
|
1266
|
+
}
|
|
1267
|
+
val totalObjects = fanout[255]
|
|
1268
|
+
|
|
1269
|
+
// Binary search for the SHA in the sorted SHA table
|
|
1270
|
+
val firstByte = shaBytes[0].toInt() and 0xFF
|
|
1271
|
+
val lo = if (firstByte == 0) 0 else fanout[firstByte - 1]
|
|
1272
|
+
val hi = fanout[firstByte]
|
|
1273
|
+
|
|
1274
|
+
// SHA table starts at offset 8 + 256*4 = 1032
|
|
1275
|
+
val shaTableStart = 8L + 256 * 4
|
|
1276
|
+
// Each SHA entry is 20 bytes
|
|
1277
|
+
var low = lo
|
|
1278
|
+
var high = hi - 1
|
|
1279
|
+
while (low <= high) {
|
|
1280
|
+
val mid = (low + high) / 2
|
|
1281
|
+
raf.seek(shaTableStart + mid * 20L)
|
|
1282
|
+
val entry = ByteArray(20)
|
|
1283
|
+
raf.readFully(entry)
|
|
1284
|
+
val cmp = compareSha(entry, shaBytes)
|
|
1285
|
+
when {
|
|
1286
|
+
cmp < 0 -> low = mid + 1
|
|
1287
|
+
cmp > 0 -> high = mid - 1
|
|
1288
|
+
else -> {
|
|
1289
|
+
// Found! Read the offset from the offset table
|
|
1290
|
+
// CRC table: after SHA table, totalObjects * 4 bytes
|
|
1291
|
+
// Offset table: after CRC table, totalObjects * 4 bytes
|
|
1292
|
+
val offsetTableStart = shaTableStart + totalObjects * 20L + totalObjects * 4L
|
|
1293
|
+
raf.seek(offsetTableStart + mid * 4L)
|
|
1294
|
+
val offset = raf.readInt().toLong() and 0xFFFFFFFFL
|
|
1295
|
+
return if (offset and 0x80000000L != 0L) {
|
|
1296
|
+
// Large offset — read from 8-byte table
|
|
1297
|
+
val largeOffsetTableStart = offsetTableStart + totalObjects * 4L
|
|
1298
|
+
val idx = (offset and 0x7FFFFFFFL).toInt()
|
|
1299
|
+
raf.seek(largeOffsetTableStart + idx * 8L)
|
|
1300
|
+
raf.readLong()
|
|
1301
|
+
} else {
|
|
1302
|
+
offset
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return null
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
/**
|
|
1312
|
+
* Read a single object from a .pack file at the given byte offset.
|
|
1313
|
+
* Handles types: commit, tree, blob, and OFS_DELTA / REF_DELTA.
|
|
1314
|
+
*/
|
|
1315
|
+
private fun readObjectFromPack(packFile: File, offset: Long, gitDir: File): ByteArray? {
|
|
1316
|
+
RandomAccessFile(packFile, "r").use { raf ->
|
|
1317
|
+
raf.seek(offset)
|
|
1318
|
+
// Read variable-length object header
|
|
1319
|
+
var byte = raf.read()
|
|
1320
|
+
val type = (byte shr 4) and 0x07
|
|
1321
|
+
var size = (byte and 0x0F).toLong()
|
|
1322
|
+
var shift = 4
|
|
1323
|
+
while (byte and 0x80 != 0) {
|
|
1324
|
+
byte = raf.read()
|
|
1325
|
+
size = size or ((byte and 0x7F).toLong() shl shift)
|
|
1326
|
+
shift += 7
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
return when (type) {
|
|
1330
|
+
1, 2, 3, 4 -> { // commit, tree, blob, tag — just decompress
|
|
1331
|
+
decompressFromRaf(raf, size)
|
|
1332
|
+
}
|
|
1333
|
+
6 -> { // OFS_DELTA
|
|
1334
|
+
// Read negative offset
|
|
1335
|
+
var b = raf.read()
|
|
1336
|
+
var deltaOffset = (b and 0x7F).toLong()
|
|
1337
|
+
while (b and 0x80 != 0) {
|
|
1338
|
+
b = raf.read()
|
|
1339
|
+
deltaOffset = ((deltaOffset + 1) shl 7) or (b and 0x7F).toLong()
|
|
1340
|
+
}
|
|
1341
|
+
val baseOffset = offset - deltaOffset
|
|
1342
|
+
val base = readObjectFromPack(packFile, baseOffset, gitDir) ?: return null
|
|
1343
|
+
val delta = decompressFromRaf(raf, size)
|
|
1344
|
+
applyDelta(base, delta)
|
|
1345
|
+
}
|
|
1346
|
+
7 -> { // REF_DELTA
|
|
1347
|
+
val baseSha = ByteArray(20)
|
|
1348
|
+
raf.readFully(baseSha)
|
|
1349
|
+
val base = readGitObject(gitDir, bytesToHex(baseSha)) ?: return null
|
|
1350
|
+
val delta = decompressFromRaf(raf, size)
|
|
1351
|
+
applyDelta(base, delta)
|
|
1352
|
+
}
|
|
1353
|
+
else -> null
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** Decompress zlib data from current RAF position */
|
|
1359
|
+
private fun decompressFromRaf(raf: RandomAccessFile, expectedSize: Long): ByteArray {
|
|
1360
|
+
// Read remaining data from current position for decompression
|
|
1361
|
+
val pos = raf.filePointer
|
|
1362
|
+
val remaining = (raf.length() - pos).coerceAtMost(expectedSize * 4 + 4096)
|
|
1363
|
+
val compressed = ByteArray(remaining.toInt())
|
|
1364
|
+
raf.readFully(compressed)
|
|
1365
|
+
val inflater = java.util.zip.Inflater()
|
|
1366
|
+
inflater.setInput(compressed)
|
|
1367
|
+
val baos = ByteArrayOutputStream(expectedSize.toInt())
|
|
1368
|
+
val buf = ByteArray(8192)
|
|
1369
|
+
while (!inflater.finished()) {
|
|
1370
|
+
val n = inflater.inflate(buf)
|
|
1371
|
+
if (n == 0 && inflater.needsInput()) break
|
|
1372
|
+
baos.write(buf, 0, n)
|
|
1373
|
+
}
|
|
1374
|
+
inflater.end()
|
|
1375
|
+
return baos.toByteArray()
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/** Apply a git delta to a base object */
|
|
1379
|
+
private fun applyDelta(base: ByteArray, delta: ByteArray): ByteArray {
|
|
1380
|
+
var pos = 0
|
|
1381
|
+
// Read base size (variable-length)
|
|
1382
|
+
var baseSize = 0L
|
|
1383
|
+
var shift = 0
|
|
1384
|
+
do {
|
|
1385
|
+
val b = delta[pos++].toInt() and 0xFF
|
|
1386
|
+
baseSize = baseSize or ((b and 0x7F).toLong() shl shift)
|
|
1387
|
+
shift += 7
|
|
1388
|
+
} while (b and 0x80 != 0)
|
|
1389
|
+
|
|
1390
|
+
// Read result size (variable-length)
|
|
1391
|
+
var resultSize = 0L
|
|
1392
|
+
shift = 0
|
|
1393
|
+
do {
|
|
1394
|
+
val b = delta[pos++].toInt() and 0xFF
|
|
1395
|
+
resultSize = resultSize or ((b and 0x7F).toLong() shl shift)
|
|
1396
|
+
shift += 7
|
|
1397
|
+
} while (b and 0x80 != 0)
|
|
1398
|
+
|
|
1399
|
+
val result = ByteArray(resultSize.toInt())
|
|
1400
|
+
var resultPos = 0
|
|
1401
|
+
|
|
1402
|
+
while (pos < delta.size) {
|
|
1403
|
+
val cmd = delta[pos++].toInt() and 0xFF
|
|
1404
|
+
if (cmd and 0x80 != 0) {
|
|
1405
|
+
// Copy from base
|
|
1406
|
+
var copyOffset = 0
|
|
1407
|
+
var copySize = 0
|
|
1408
|
+
if (cmd and 0x01 != 0) copyOffset = delta[pos++].toInt() and 0xFF
|
|
1409
|
+
if (cmd and 0x02 != 0) copyOffset = copyOffset or ((delta[pos++].toInt() and 0xFF) shl 8)
|
|
1410
|
+
if (cmd and 0x04 != 0) copyOffset = copyOffset or ((delta[pos++].toInt() and 0xFF) shl 16)
|
|
1411
|
+
if (cmd and 0x08 != 0) copyOffset = copyOffset or ((delta[pos++].toInt() and 0xFF) shl 24)
|
|
1412
|
+
if (cmd and 0x10 != 0) copySize = delta[pos++].toInt() and 0xFF
|
|
1413
|
+
if (cmd and 0x20 != 0) copySize = copySize or ((delta[pos++].toInt() and 0xFF) shl 8)
|
|
1414
|
+
if (cmd and 0x40 != 0) copySize = copySize or ((delta[pos++].toInt() and 0xFF) shl 16)
|
|
1415
|
+
if (copySize == 0) copySize = 0x10000
|
|
1416
|
+
System.arraycopy(base, copyOffset, result, resultPos, copySize)
|
|
1417
|
+
resultPos += copySize
|
|
1418
|
+
} else if (cmd != 0) {
|
|
1419
|
+
// Insert literal data
|
|
1420
|
+
System.arraycopy(delta, pos, result, resultPos, cmd)
|
|
1421
|
+
pos += cmd
|
|
1422
|
+
resultPos += cmd
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
return result
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/** Parse a commit object to extract the tree SHA (first line: "tree <sha>\n") */
|
|
1429
|
+
private fun parseCommitTreeSha(commitData: ByteArray): String? {
|
|
1430
|
+
val str = String(commitData, Charsets.UTF_8)
|
|
1431
|
+
for (line in str.lineSequence()) {
|
|
1432
|
+
if (line.startsWith("tree ")) {
|
|
1433
|
+
return line.removePrefix("tree ").trim()
|
|
1434
|
+
}
|
|
1435
|
+
if (line.isBlank()) break
|
|
1436
|
+
}
|
|
1437
|
+
return null
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Parse a tree object into a list of (name, mode, sha-bytes) entries.
|
|
1442
|
+
*
|
|
1443
|
+
* Tree format: repeated entries of "mode name\0<20-byte SHA>"
|
|
1444
|
+
*/
|
|
1445
|
+
private fun parseTreeEntries(treeData: ByteArray): List<Triple<String, Int, ByteArray>> {
|
|
1446
|
+
val entries = mutableListOf<Triple<String, Int, ByteArray>>()
|
|
1447
|
+
var pos = 0
|
|
1448
|
+
while (pos < treeData.size) {
|
|
1449
|
+
// Read "mode " (space-separated)
|
|
1450
|
+
var spaceIdx = pos
|
|
1451
|
+
while (spaceIdx < treeData.size && treeData[spaceIdx] != ' '.code.toByte()) spaceIdx++
|
|
1452
|
+
if (spaceIdx >= treeData.size) break
|
|
1453
|
+
val modeStr = String(treeData, pos, spaceIdx - pos, Charsets.US_ASCII)
|
|
1454
|
+
val mode = modeStr.toInt(8)
|
|
1455
|
+
pos = spaceIdx + 1
|
|
1456
|
+
|
|
1457
|
+
// Read name until NUL
|
|
1458
|
+
var nullIdx = pos
|
|
1459
|
+
while (nullIdx < treeData.size && treeData[nullIdx] != 0.toByte()) nullIdx++
|
|
1460
|
+
if (nullIdx >= treeData.size) break
|
|
1461
|
+
val name = String(treeData, pos, nullIdx - pos, Charsets.UTF_8)
|
|
1462
|
+
pos = nullIdx + 1
|
|
1463
|
+
|
|
1464
|
+
// Read 20-byte SHA
|
|
1465
|
+
if (pos + 20 > treeData.size) break
|
|
1466
|
+
val sha = treeData.copyOfRange(pos, pos + 20)
|
|
1467
|
+
pos += 20
|
|
1468
|
+
|
|
1469
|
+
entries.add(Triple(name, mode, sha))
|
|
1470
|
+
}
|
|
1471
|
+
return entries
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
/** Data class for a tree entry (file path, mode, blob SHA bytes) */
|
|
1475
|
+
private data class GitTreeEntry(val path: String, val mode: Int, val sha: ByteArray)
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Build a version 2 .git/index binary from tree entries and disk stats.
|
|
1479
|
+
*
|
|
1480
|
+
* Index v2 format:
|
|
1481
|
+
* - 12-byte header: "DIRC" + version(4) + numEntries(4)
|
|
1482
|
+
* - Sorted entries, each:
|
|
1483
|
+
* ctime_s(4) + ctime_ns(4) + mtime_s(4) + mtime_ns(4) +
|
|
1484
|
+
* dev(4) + ino(4) + mode(4) + uid(4) + gid(4) +
|
|
1485
|
+
* file_size(4) + SHA-1(20) + flags(2) + path(variable) + NUL padding to 8-byte boundary
|
|
1486
|
+
* - 20-byte SHA-1 checksum over the entire index (header + entries)
|
|
1487
|
+
*/
|
|
1488
|
+
private fun buildIndexBinary(root: File, entries: List<GitTreeEntry>): ByteArray {
|
|
1489
|
+
val baos = ByteArrayOutputStream()
|
|
1490
|
+
|
|
1491
|
+
fun writeInt32(value: Int) {
|
|
1492
|
+
baos.write((value shr 24) and 0xFF)
|
|
1493
|
+
baos.write((value shr 16) and 0xFF)
|
|
1494
|
+
baos.write((value shr 8) and 0xFF)
|
|
1495
|
+
baos.write(value and 0xFF)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Header: "DIRC" + version 2 + entry count
|
|
1499
|
+
baos.write("DIRC".toByteArray(Charsets.US_ASCII))
|
|
1500
|
+
writeInt32(2) // version
|
|
1501
|
+
writeInt32(entries.size)
|
|
1502
|
+
|
|
1503
|
+
for (entry in entries) {
|
|
1504
|
+
val workFile = File(root, entry.path)
|
|
1505
|
+
val mtimeMs = if (workFile.exists()) workFile.lastModified() else 0L
|
|
1506
|
+
val mtimeS = (mtimeMs / 1000).toInt()
|
|
1507
|
+
val mtimeNs = ((mtimeMs % 1000) * 1_000_000).toInt()
|
|
1508
|
+
val fileSize = if (workFile.exists()) workFile.length().toInt() else 0
|
|
1509
|
+
|
|
1510
|
+
// ctime (same as mtime for simplicity)
|
|
1511
|
+
writeInt32(mtimeS)
|
|
1512
|
+
writeInt32(mtimeNs)
|
|
1513
|
+
// mtime
|
|
1514
|
+
writeInt32(mtimeS)
|
|
1515
|
+
writeInt32(mtimeNs)
|
|
1516
|
+
// dev, ino (0 — not available on Android)
|
|
1517
|
+
writeInt32(0)
|
|
1518
|
+
writeInt32(0)
|
|
1519
|
+
// mode: entry.mode is octal (e.g., 0o100644), needs to be stored as-is
|
|
1520
|
+
writeInt32(entry.mode)
|
|
1521
|
+
// uid, gid (0)
|
|
1522
|
+
writeInt32(0)
|
|
1523
|
+
writeInt32(0)
|
|
1524
|
+
// file size
|
|
1525
|
+
writeInt32(fileSize)
|
|
1526
|
+
// 20-byte SHA-1 of the blob
|
|
1527
|
+
baos.write(entry.sha)
|
|
1528
|
+
// flags: assume flag = (pathLen & 0xFFF)
|
|
1529
|
+
val pathBytes = entry.path.toByteArray(Charsets.UTF_8)
|
|
1530
|
+
val flags = pathBytes.size.coerceAtMost(0xFFF)
|
|
1531
|
+
baos.write((flags shr 8) and 0xFF)
|
|
1532
|
+
baos.write(flags and 0xFF)
|
|
1533
|
+
// path + NUL + padding to 8-byte boundary
|
|
1534
|
+
baos.write(pathBytes)
|
|
1535
|
+
baos.write(0) // NUL terminator
|
|
1536
|
+
// Pad to 8-byte boundary (entry starts at header end or previous entry end)
|
|
1537
|
+
// Total entry size so far = 62 + pathBytes.size + 1
|
|
1538
|
+
val entrySize = 62 + pathBytes.size + 1
|
|
1539
|
+
val padding = (8 - (entrySize % 8)) % 8
|
|
1540
|
+
for (i in 0 until padding) {
|
|
1541
|
+
baos.write(0)
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
val content = baos.toByteArray()
|
|
1546
|
+
|
|
1547
|
+
// Compute SHA-1 checksum over everything
|
|
1548
|
+
val digest = java.security.MessageDigest.getInstance("SHA-1")
|
|
1549
|
+
val checksum = digest.digest(content)
|
|
1550
|
+
|
|
1551
|
+
// Final result: content + checksum
|
|
1552
|
+
val result = ByteArrayOutputStream(content.size + 20)
|
|
1553
|
+
result.write(content)
|
|
1554
|
+
result.write(checksum)
|
|
1555
|
+
return result.toByteArray()
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1064
1558
|
// ─── Git index parser ─────────────────────────────────────────────
|
|
1065
1559
|
|
|
1066
1560
|
/**
|
package/build/index.d.ts
CHANGED
|
@@ -133,6 +133,22 @@ interface IExternalStorageModule {
|
|
|
133
133
|
* @returns JSON string: `[{"path":"tiddlers/foo.tid","type":"add"|"modify"|"delete"}, ...]`
|
|
134
134
|
*/
|
|
135
135
|
gitStatus(gitRootDir: string): Promise<string>;
|
|
136
|
+
/**
|
|
137
|
+
* Debug function returning diagnostic info about the git repository state.
|
|
138
|
+
* @returns JSON string with root/gitDir/index existence and git dir children
|
|
139
|
+
*/
|
|
140
|
+
gitStatusDebug(gitRootDir: string): Promise<string>;
|
|
141
|
+
/**
|
|
142
|
+
* Build `.git/index` natively by reading the HEAD tree from pack files,
|
|
143
|
+
* stat'ing all files on disk, and writing a v2 index file.
|
|
144
|
+
*
|
|
145
|
+
* This is used after archive clone where TidGi Desktop's tar export
|
|
146
|
+
* doesn't include `.git/index`.
|
|
147
|
+
*
|
|
148
|
+
* @param gitRootDir The root directory of the git repository (parent of .git/)
|
|
149
|
+
* @returns JSON string: `{"ok":true,"entries":N,"indexSize":M}` or `{"ok":false,"error":"..."}`
|
|
150
|
+
*/
|
|
151
|
+
buildGitIndex(gitRootDir: string): Promise<string>;
|
|
136
152
|
}
|
|
137
153
|
export declare const ExternalStorage: IExternalStorageModule;
|
|
138
154
|
/**
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,IAAI,OAA2C,CAAC;AAEhD;;;;GAIG;AACH,SAAS,eAAe;IACtB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,iEAAiE;IACjE,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,CAAsE,CAAC;IAClI,OAAO,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC;AACjB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,IAAI,OAA2C,CAAC;AAEhD;;;;GAIG;AACH,SAAS,eAAe;IACtB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,iEAAiE;IACjE,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,CAAsE,CAAC;IAClI,OAAO,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC;AACjB,CAAC;AAsLD,MAAM,CAAC,MAAM,eAAe,GAA2B,IAAI,KAAK,CAAC,EAA4B,EAAE;IAC7F,GAAG,CAAC,OAAO,EAAE,QAAQ;QACnB,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;QAC9B,OAAQ,GAAmD,CAAC,QAAQ,CAAC,CAAC;IACxE,CAAC;CACF,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB;IAC3C,IAAI,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["/**\n * TypeScript bindings for the ExternalStorage native module.\n *\n * This module uses raw java.io.File on Android to bypass Expo FileSystem's\n * directory whitelist. It allows reading/writing to shared external storage\n * when MANAGE_EXTERNAL_STORAGE permission is granted.\n *\n * All path arguments are plain filesystem paths (e.g. \"/storage/emulated/0/Documents/TidGi/\").\n * Do NOT pass file:// URIs — strip the scheme before calling.\n */\nimport { Platform } from 'react-native';\n\nlet _module: IExternalStorageModule | undefined;\n\n/**\n * Lazily load the native module. Wrapped in a function so that the app does NOT\n * crash at import time if the native module is missing (e.g. on iOS or when the\n * binary was built without it).\n */\nfunction getNativeModule(): IExternalStorageModule {\n if (_module) return _module;\n if (Platform.OS !== 'android') {\n throw new Error('ExternalStorage native module is only available on Android');\n }\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { requireNativeModule } = require('expo-modules-core') as { requireNativeModule: (name: string) => IExternalStorageModule };\n _module = requireNativeModule('ExternalStorage');\n return _module;\n}\n\ninterface FileInfo {\n exists: boolean;\n isDirectory: boolean;\n size: number;\n /** Milliseconds since epoch */\n modificationTime: number;\n}\n\ninterface BatchWriteResult {\n writtenCount: number;\n}\n\ninterface HttpPostToFileResult {\n statusCode: number;\n headers: Record<string, string>;\n bytesWritten: number;\n}\n\ninterface DownloadFileResumableResult {\n statusCode: number;\n /** Final size of the file on disk after download */\n totalBytes: number;\n /** true if the download resumed from a partial file (HTTP 206) */\n resumed: boolean;\n}\n\ninterface ExtractTarResult {\n filesExtracted: number;\n}\n\ninterface ReadFileChunkResult {\n /** Base64-encoded chunk data */\n data: string;\n bytesRead: number;\n}\n\ninterface IExternalStorageModule {\n exists(path: string): Promise<boolean>;\n getInfo(path: string): Promise<FileInfo>;\n\n mkdir(path: string): Promise<void>;\n readDir(path: string): Promise<string[]>;\n /** Recursively list all files under a directory, returning relative paths. Skips .git etc. */\n readDirRecursive(path: string): Promise<string[]>;\n rmdir(path: string): Promise<void>;\n\n readFileUtf8(path: string): Promise<string>;\n readFileBase64(path: string): Promise<string>;\n writeFileUtf8(path: string, content: string): Promise<void>;\n writeFileBase64(path: string, base64Content: string): Promise<void>;\n /**\n * Append a Base64-encoded chunk to a file, optionally truncating first.\n *\n * Designed for streaming large writes from JS in bounded-memory chunks\n * (e.g. 512 KB each) so the JVM never allocates the full file content,\n * avoiding OOM on 50+ MB git pack files.\n *\n * @param truncateFirst Pass `true` for the first chunk to create/truncate\n * the file, then `false` for subsequent chunks.\n */\n appendFileBase64(path: string, base64Content: string, truncateFirst: boolean): Promise<void>;\n writeFilesBase64(paths: string[], base64Contents: string[]): Promise<BatchWriteResult>;\n deleteFile(path: string): Promise<void>;\n\n isExternalStorageWritable(): Promise<boolean>;\n getExternalStorageDirectory(): Promise<string>;\n /** Android 11+ (API 30): check if MANAGE_EXTERNAL_STORAGE is granted. Pre-30 returns true. */\n isExternalStorageManager(): Promise<boolean>;\n\n /**\n * HTTP POST with the response body streamed directly to a file on disk,\n * **never buffering the full response in JVM/Hermes heap**.\n *\n * Designed for git-upload-pack which can return 100+ MB packfiles.\n *\n * @param url Target URL\n * @param headers HTTP headers as `{ key: value }`\n * @param bodyBase64 Request body encoded as Base64 (binary git protocol data)\n * @param destPath Plain filesystem path to write the response body to\n * @param contentType MIME type for the request body\n */\n httpPostToFile(\n url: string,\n headers: Record<string, string>,\n bodyBase64: string,\n destPath: string,\n contentType: string,\n ): Promise<HttpPostToFileResult>;\n\n /**\n * Read a chunk of a file starting at `offset` for up to `length` bytes.\n * Returns Base64-encoded data and actual bytes read.\n *\n * Use this to stream a large file into JS in bounded-memory chunks.\n */\n readFileChunk(path: string, offset: number, length: number): Promise<ReadFileChunkResult>;\n\n /**\n * Download a file via HTTP GET with resumable download support.\n *\n * If `destPath` already exists on disk (from a previous interrupted download),\n * sends `Range: bytes=<existingSize>-` to resume. The server must respond\n * with 206 Partial Content for resume to work; otherwise the file is\n * overwritten from scratch (200 response).\n *\n * @param url Target URL\n * @param headers Extra HTTP headers (e.g. Authorization, ETag)\n * @param destPath Plain filesystem path for the downloaded file\n */\n downloadFileResumable(\n url: string,\n headers: Record<string, string>,\n destPath: string,\n ): Promise<DownloadFileResumableResult>;\n\n /**\n * Extract an uncompressed tar archive to a destination directory.\n * Uses a native tar parser — no third-party dependency.\n * Supports POSIX ustar and GNU long-name extensions.\n * Validates paths to prevent directory traversal attacks.\n *\n * @param tarPath Path to the .tar file\n * @param destDir Destination directory (created if needed)\n */\n extractTar(tarPath: string, destDir: string): Promise<ExtractTarResult>;\n\n /**\n * Parse a batch of TiddlyWiki tiddler files entirely in native Kotlin.\n *\n * This is the critical performance optimization for initial wiki loading:\n * a single bridge call processes 100+ files in parallel, returning a\n * ready-to-inject JSON array string. Eliminates per-file bridge round-trips.\n *\n * Supports .tid, .json, and .meta files. Applies skinny logic:\n * - System tiddlers ($:/) → always full text\n * - Plugins (application/json + plugin-type) → always full text\n * - Module tiddlers (module-type) → always full text\n * - Small tiddlers (< 10KB body) → full text\n * - Large user tiddlers → skinny (_is_skinny: \"yes\", text omitted)\n *\n * @param filePaths Array of absolute filesystem paths\n * @param quickLoadMode If true, all tiddlers returned as skinny\n * @returns JSON string: serialized array of tiddler field objects\n */\n batchParseTidFiles(filePaths: string[], quickLoadMode: boolean): Promise<string>;\n\n /**\n * Lightweight native git status using direct git-index parsing.\n *\n * Parses `.git/index` to get tracked files and their stat-cache entries,\n * then compares against the working directory using file size and mtime.\n * Orders of magnitude faster than isomorphic-git's `statusMatrix` because:\n * - No JS↔Native bridge round-trips per file\n * - Uses stat-cache (size+mtime) instead of SHA-1 re-hashing\n * - Parallel file walking in Java\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `[{\"path\":\"tiddlers/foo.tid\",\"type\":\"add\"|\"modify\"|\"delete\"}, ...]`\n */\n gitStatus(gitRootDir: string): Promise<string>;\n\n /**\n * Debug function returning diagnostic info about the git repository state.\n * @returns JSON string with root/gitDir/index existence and git dir children\n */\n gitStatusDebug(gitRootDir: string): Promise<string>;\n\n /**\n * Build `.git/index` natively by reading the HEAD tree from pack files,\n * stat'ing all files on disk, and writing a v2 index file.\n *\n * This is used after archive clone where TidGi Desktop's tar export\n * doesn't include `.git/index`.\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `{\"ok\":true,\"entries\":N,\"indexSize\":M}` or `{\"ok\":false,\"error\":\"...\"}`\n */\n buildGitIndex(gitRootDir: string): Promise<string>;\n}\n\nexport const ExternalStorage: IExternalStorageModule = new Proxy({} as IExternalStorageModule, {\n get(_target, property) {\n const mod = getNativeModule();\n return (mod as unknown as Record<string | symbol, unknown>)[property];\n },\n});\n\n/**\n * Strip file:// prefix from a URI to produce a plain filesystem path.\n * Safe to call on paths that are already plain.\n */\nexport function toPlainPath(uriOrPath: string): string {\n if (uriOrPath.startsWith('file://')) {\n return uriOrPath.slice('file://'.length);\n }\n return uriOrPath;\n}\n\nexport type { BatchWriteResult, DownloadFileResumableResult, ExtractTarResult, FileInfo, HttpPostToFileResult, IExternalStorageModule, ReadFileChunkResult };\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-tiddlywiki-filesystem-android-external-storage",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "Expo native module for TidGi-Mobile: external storage I/O + TiddlyWiki .tid/.meta/.json batch parsing in Kotlin",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -188,6 +188,24 @@ interface IExternalStorageModule {
|
|
|
188
188
|
* @returns JSON string: `[{"path":"tiddlers/foo.tid","type":"add"|"modify"|"delete"}, ...]`
|
|
189
189
|
*/
|
|
190
190
|
gitStatus(gitRootDir: string): Promise<string>;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Debug function returning diagnostic info about the git repository state.
|
|
194
|
+
* @returns JSON string with root/gitDir/index existence and git dir children
|
|
195
|
+
*/
|
|
196
|
+
gitStatusDebug(gitRootDir: string): Promise<string>;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build `.git/index` natively by reading the HEAD tree from pack files,
|
|
200
|
+
* stat'ing all files on disk, and writing a v2 index file.
|
|
201
|
+
*
|
|
202
|
+
* This is used after archive clone where TidGi Desktop's tar export
|
|
203
|
+
* doesn't include `.git/index`.
|
|
204
|
+
*
|
|
205
|
+
* @param gitRootDir The root directory of the git repository (parent of .git/)
|
|
206
|
+
* @returns JSON string: `{"ok":true,"entries":N,"indexSize":M}` or `{"ok":false,"error":"..."}`
|
|
207
|
+
*/
|
|
208
|
+
buildGitIndex(gitRootDir: string): Promise<string>;
|
|
191
209
|
}
|
|
192
210
|
|
|
193
211
|
export const ExternalStorage: IExternalStorageModule = new Proxy({} as IExternalStorageModule, {
|