JsExtensions.kt 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002
  1. package io.legato.kazusa.help
  2. import android.webkit.WebSettings
  3. import androidx.annotation.Keep
  4. import cn.hutool.core.codec.Base64
  5. import cn.hutool.core.util.HexUtil
  6. import com.script.rhino.rhinoContext
  7. import com.script.rhino.rhinoContextOrNull
  8. import io.legato.kazusa.constant.AppConst
  9. import io.legato.kazusa.constant.AppConst.dateFormat
  10. import io.legato.kazusa.constant.AppLog
  11. import io.legato.kazusa.constant.AppPattern
  12. import io.legato.kazusa.data.entities.BaseSource
  13. import io.legato.kazusa.exception.NoStackTraceException
  14. import io.legato.kazusa.help.config.AppConfig
  15. import io.legato.kazusa.help.http.BackstageWebView
  16. import io.legato.kazusa.help.http.CookieManager.cookieJarHeader
  17. import io.legato.kazusa.help.http.CookieStore
  18. import io.legato.kazusa.help.http.SSLHelper
  19. import io.legato.kazusa.help.http.StrResponse
  20. import io.legato.kazusa.help.source.SourceVerificationHelp
  21. import io.legato.kazusa.help.source.getSourceType
  22. import io.legato.kazusa.model.Debug
  23. import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
  24. import io.legato.kazusa.model.analyzeRule.QueryTTF
  25. import io.legato.kazusa.ui.association.OpenUrlConfirmActivity
  26. import io.legato.kazusa.utils.ArchiveUtils
  27. import io.legato.kazusa.utils.ChineseUtils
  28. import io.legato.kazusa.utils.EncoderUtils
  29. import io.legato.kazusa.utils.EncodingDetect
  30. import io.legato.kazusa.utils.FileUtils
  31. import io.legato.kazusa.utils.GSON
  32. import io.legato.kazusa.utils.HtmlFormatter
  33. import io.legato.kazusa.utils.JsURL
  34. import io.legato.kazusa.utils.MD5Utils
  35. import io.legato.kazusa.utils.StringUtils
  36. import io.legato.kazusa.utils.UrlUtil
  37. import io.legato.kazusa.utils.compress.LibArchiveUtils
  38. import io.legato.kazusa.utils.createFileReplace
  39. import io.legato.kazusa.utils.externalCache
  40. import io.legato.kazusa.utils.fromJsonObject
  41. import io.legato.kazusa.utils.isAbsUrl
  42. import io.legato.kazusa.utils.isMainThread
  43. import io.legato.kazusa.utils.longToastForJs
  44. import io.legato.kazusa.utils.mapAsync
  45. import io.legato.kazusa.utils.stackTraceStr
  46. import io.legato.kazusa.utils.startActivity
  47. import io.legato.kazusa.utils.toStringArray
  48. import io.legato.kazusa.utils.toastForJs
  49. import kotlinx.coroutines.Dispatchers.IO
  50. import kotlinx.coroutines.flow.asFlow
  51. import kotlinx.coroutines.flow.flowOn
  52. import kotlinx.coroutines.flow.toList
  53. import kotlinx.coroutines.runBlocking
  54. import okio.use
  55. import org.jsoup.Connection
  56. import org.jsoup.Jsoup
  57. import splitties.init.appCtx
  58. import java.io.ByteArrayInputStream
  59. import java.io.ByteArrayOutputStream
  60. import java.io.File
  61. import java.net.URLEncoder
  62. import java.nio.charset.Charset
  63. import java.security.MessageDigest
  64. import java.text.SimpleDateFormat
  65. import java.util.Date
  66. import java.util.Locale
  67. import java.util.SimpleTimeZone
  68. import java.util.UUID
  69. import java.util.zip.ZipEntry
  70. import java.util.zip.ZipInputStream
  71. import kotlin.coroutines.CoroutineContext
  72. import kotlin.coroutines.EmptyCoroutineContext
  73. /**
  74. * js扩展类, 在js中通过java变量调用
  75. * 添加方法,请更新文档/legado/app/src/main/assets/help/JsHelp.md
  76. * 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件
  77. * /android/data/{package}/cache/...
  78. */
  79. @Keep
  80. @Suppress("unused")
  81. interface JsExtensions : JsEncodeUtils {
  82. fun getSource(): BaseSource?
  83. private val context: CoroutineContext
  84. get() = rhinoContext.coroutineContext ?: EmptyCoroutineContext
  85. /**
  86. * 访问网络,返回String
  87. */
  88. fun ajax(url: Any): String? {
  89. val urlStr = if (url is List<*>) {
  90. url.firstOrNull().toString()
  91. } else {
  92. url.toString()
  93. }
  94. val analyzeUrl = AnalyzeUrl(urlStr, source = getSource(), coroutineContext = context)
  95. return kotlin.runCatching {
  96. analyzeUrl.getStrResponse().body
  97. }.onFailure {
  98. rhinoContext.ensureActive()
  99. AppLog.put("ajax(${urlStr}) error\n${it.localizedMessage}", it)
  100. }.getOrElse {
  101. it.stackTraceStr
  102. }
  103. }
  104. /**
  105. * 并发访问网络
  106. */
  107. fun ajaxAll(urlList: Array<String>): Array<StrResponse> {
  108. return runBlocking(context) {
  109. urlList.asFlow().mapAsync(AppConfig.threadCount) { url ->
  110. val analyzeUrl = AnalyzeUrl(
  111. url,
  112. source = getSource(),
  113. coroutineContext = coroutineContext
  114. )
  115. analyzeUrl.getStrResponseAwait()
  116. }.flowOn(IO).toList().toTypedArray()
  117. }
  118. }
  119. /**
  120. * 访问网络,返回Response<String>
  121. */
  122. fun connect(urlStr: String): StrResponse {
  123. val analyzeUrl = AnalyzeUrl(
  124. urlStr,
  125. source = getSource(),
  126. coroutineContext = context
  127. )
  128. return kotlin.runCatching {
  129. analyzeUrl.getStrResponse()
  130. }.onFailure {
  131. rhinoContext.ensureActive()
  132. AppLog.put("connect(${urlStr}) error\n${it.localizedMessage}", it)
  133. }.getOrElse {
  134. StrResponse(analyzeUrl.url, it.stackTraceStr)
  135. }
  136. }
  137. fun connect(urlStr: String, header: String?): StrResponse {
  138. val headerMap = GSON.fromJsonObject<Map<String, String>>(header).getOrNull()
  139. val analyzeUrl = AnalyzeUrl(
  140. urlStr,
  141. headerMapF = headerMap,
  142. source = getSource(),
  143. coroutineContext = context
  144. )
  145. return kotlin.runCatching {
  146. analyzeUrl.getStrResponse()
  147. }.onFailure {
  148. rhinoContext.ensureActive()
  149. AppLog.put("ajax($urlStr,$header) error\n${it.localizedMessage}", it)
  150. }.getOrElse {
  151. StrResponse(analyzeUrl.url, it.stackTraceStr)
  152. }
  153. }
  154. /**
  155. * 使用webView访问网络
  156. * @param html 直接用webView载入的html, 如果html为空直接访问url
  157. * @param url html内如果有相对路径的资源不传入url访问不了
  158. * @param js 用来取返回值的js语句, 没有就返回整个源代码
  159. * @return 返回js获取的内容
  160. */
  161. fun webView(html: String?, url: String?, js: String?): String? {
  162. if (isMainThread) {
  163. error("webView must be called on a background thread")
  164. }
  165. return runBlocking(context) {
  166. BackstageWebView(
  167. url = url,
  168. html = html,
  169. javaScript = js,
  170. headerMap = getSource()?.getHeaderMap(true),
  171. tag = getSource()?.getKey()
  172. ).getStrResponse().body
  173. }
  174. }
  175. /**
  176. * 使用webView获取资源url
  177. */
  178. fun webViewGetSource(html: String?, url: String?, js: String?, sourceRegex: String): String? {
  179. if (isMainThread) {
  180. error("webViewGetSource must be called on a background thread")
  181. }
  182. return runBlocking(context) {
  183. BackstageWebView(
  184. url = url,
  185. html = html,
  186. javaScript = js,
  187. headerMap = getSource()?.getHeaderMap(true),
  188. tag = getSource()?.getKey(),
  189. sourceRegex = sourceRegex
  190. ).getStrResponse().body
  191. }
  192. }
  193. /**
  194. * 使用webView获取跳转url
  195. */
  196. fun webViewGetOverrideUrl(
  197. html: String?,
  198. url: String?,
  199. js: String?,
  200. overrideUrlRegex: String
  201. ): String? {
  202. if (isMainThread) {
  203. error("webViewGetOverrideUrl must be called on a background thread")
  204. }
  205. return runBlocking(context) {
  206. BackstageWebView(
  207. url = url,
  208. html = html,
  209. javaScript = js,
  210. headerMap = getSource()?.getHeaderMap(true),
  211. tag = getSource()?.getKey(),
  212. overrideUrlRegex = overrideUrlRegex
  213. ).getStrResponse().body
  214. }
  215. }
  216. /**
  217. * 使用内置浏览器打开链接,手动验证网站防爬
  218. * @param url 要打开的链接
  219. * @param title 浏览器页面的标题
  220. */
  221. fun startBrowser(url: String, title: String) {
  222. rhinoContext.ensureActive()
  223. SourceVerificationHelp.startBrowser(getSource(), url, title)
  224. }
  225. /**
  226. * 使用内置浏览器打开链接,并等待网页结果
  227. */
  228. fun startBrowserAwait(url: String, title: String, refetchAfterSuccess: Boolean): StrResponse {
  229. rhinoContext.ensureActive()
  230. val body = SourceVerificationHelp.getVerificationResult(
  231. getSource(), url, title, true, refetchAfterSuccess
  232. )
  233. return StrResponse(url, body)
  234. }
  235. fun startBrowserAwait(url: String, title: String): StrResponse {
  236. return startBrowserAwait(url, title, true)
  237. }
  238. /**
  239. * 打开图片验证码对话框,等待返回验证结果
  240. */
  241. fun getVerificationCode(imageUrl: String): String {
  242. rhinoContext.ensureActive()
  243. return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
  244. }
  245. /**
  246. * 可从网络,本地文件(阅读私有数据目录相对路径)导入JavaScript脚本
  247. */
  248. fun importScript(path: String): String {
  249. val result = when {
  250. path.startsWith("http") -> cacheFile(path)
  251. else -> readTxtFile(path)
  252. }
  253. if (result.isBlank()) throw NoStackTraceException("$path 内容获取失败或者为空")
  254. return result
  255. }
  256. /**
  257. * 缓存以文本方式保存的文件 如.js .txt等
  258. * @param urlStr 网络文件的链接
  259. * @return 返回缓存后的文件内容
  260. */
  261. fun cacheFile(urlStr: String): String {
  262. return cacheFile(urlStr, 0)
  263. }
  264. /**
  265. * 缓存以文本方式保存的文件 如.js .txt等
  266. * @param saveTime 缓存时间,单位:秒
  267. */
  268. fun cacheFile(urlStr: String, saveTime: Int): String {
  269. val key = md5Encode16(urlStr)
  270. val cachePath = CacheManager.get(key)
  271. return if (
  272. cachePath.isNullOrBlank() ||
  273. !getFile(cachePath).exists()
  274. ) {
  275. val path = downloadFile(urlStr)
  276. log("首次下载 $urlStr >> $path")
  277. CacheManager.put(key, path, saveTime)
  278. readTxtFile(path)
  279. } else {
  280. readTxtFile(cachePath)
  281. }
  282. }
  283. /**
  284. *js实现读取cookie
  285. */
  286. fun getCookie(tag: String): String {
  287. return getCookie(tag, null)
  288. }
  289. fun getCookie(tag: String, key: String?): String {
  290. return if (key != null) {
  291. CookieStore.getKey(tag, key)
  292. } else {
  293. CookieStore.getCookie(tag)
  294. }
  295. }
  296. /**
  297. * 下载文件
  298. * @param url 下载地址:可带参数type
  299. * @return 下载的文件相对路径
  300. */
  301. fun downloadFile(url: String): String {
  302. rhinoContext.ensureActive()
  303. val analyzeUrl = AnalyzeUrl(url, source = getSource(), coroutineContext = context)
  304. val type = UrlUtil.getSuffix(url, analyzeUrl.type)
  305. val path = FileUtils.getPath(
  306. File(FileUtils.getCachePath()),
  307. "${MD5Utils.md5Encode16(url)}.${type}"
  308. )
  309. val file = File(path)
  310. file.delete()
  311. analyzeUrl.getInputStream().use { iStream ->
  312. file.createFileReplace()
  313. try {
  314. file.outputStream().buffered().use { oStream ->
  315. iStream.copyTo(oStream)
  316. }
  317. } catch (e: Throwable) {
  318. file.delete()
  319. throw e
  320. }
  321. }
  322. return path.substring(FileUtils.getCachePath().length)
  323. }
  324. /**
  325. * 实现16进制字符串转文件
  326. * @param content 需要转成文件的16进制字符串
  327. * @param url 通过url里的参数来判断文件类型
  328. * @return 相对路径
  329. */
  330. @Deprecated(
  331. "Deprecated",
  332. ReplaceWith("downloadFile(url)")
  333. )
  334. fun downloadFile(content: String, url: String): String {
  335. rhinoContext.ensureActive()
  336. val type = AnalyzeUrl(url, source = getSource(), coroutineContext = context).type
  337. ?: return ""
  338. val path = FileUtils.getPath(
  339. FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
  340. "${MD5Utils.md5Encode16(url)}.${type}"
  341. )
  342. val file = File(path)
  343. file.createFileReplace()
  344. HexUtil.decodeHex(content).let {
  345. if (it.isNotEmpty()) {
  346. file.writeBytes(it)
  347. }
  348. }
  349. return path.substring(FileUtils.getCachePath().length)
  350. }
  351. /**
  352. * js实现重定向拦截,网络访问get
  353. */
  354. fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
  355. val requestHeaders = if (getSource()?.enabledCookieJar == true) {
  356. headers.toMutableMap().apply { put(cookieJarHeader, "1") }
  357. } else headers
  358. val rateLimiter = ConcurrentRateLimiter(getSource())
  359. val response = rateLimiter.withLimitBlocking {
  360. rhinoContext.ensureActive()
  361. Jsoup.connect(urlStr)
  362. .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
  363. .ignoreContentType(true)
  364. .followRedirects(false)
  365. .headers(requestHeaders)
  366. .method(Connection.Method.GET)
  367. .execute()
  368. }
  369. return response
  370. }
  371. /**
  372. * js实现重定向拦截,网络访问head,不返回Response Body更省流量
  373. */
  374. fun head(urlStr: String, headers: Map<String, String>): Connection.Response {
  375. val requestHeaders = if (getSource()?.enabledCookieJar == true) {
  376. headers.toMutableMap().apply { put(cookieJarHeader, "1") }
  377. } else headers
  378. val rateLimiter = ConcurrentRateLimiter(getSource())
  379. val response = rateLimiter.withLimitBlocking {
  380. rhinoContext.ensureActive()
  381. Jsoup.connect(urlStr)
  382. .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
  383. .ignoreContentType(true)
  384. .followRedirects(false)
  385. .headers(requestHeaders)
  386. .method(Connection.Method.HEAD)
  387. .execute()
  388. }
  389. return response
  390. }
  391. /**
  392. * 网络访问post
  393. */
  394. fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
  395. val requestHeaders = if (getSource()?.enabledCookieJar == true) {
  396. headers.toMutableMap().apply { put(cookieJarHeader, "1") }
  397. } else headers
  398. val rateLimiter = ConcurrentRateLimiter(getSource())
  399. val response = rateLimiter.withLimitBlocking {
  400. rhinoContext.ensureActive()
  401. Jsoup.connect(urlStr)
  402. .sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
  403. .ignoreContentType(true)
  404. .followRedirects(false)
  405. .requestBody(body)
  406. .headers(requestHeaders)
  407. .method(Connection.Method.POST)
  408. .execute()
  409. }
  410. return response
  411. }
  412. /* Str转ByteArray */
  413. fun strToBytes(str: String): ByteArray {
  414. return str.toByteArray(charset("UTF-8"))
  415. }
  416. fun strToBytes(str: String, charset: String): ByteArray {
  417. return str.toByteArray(charset(charset))
  418. }
  419. /* ByteArray转Str */
  420. fun bytesToStr(bytes: ByteArray): String {
  421. return String(bytes, charset("UTF-8"))
  422. }
  423. fun bytesToStr(bytes: ByteArray, charset: String): String {
  424. return String(bytes, charset(charset))
  425. }
  426. /**
  427. * js实现base64解码,不能删
  428. */
  429. fun base64Decode(str: String?): String {
  430. return Base64.decodeStr(str)
  431. }
  432. fun base64Decode(str: String?, charset: String): String {
  433. return Base64.decodeStr(str, charset(charset))
  434. }
  435. fun base64Decode(str: String, flags: Int): String {
  436. return EncoderUtils.base64Decode(str, flags)
  437. }
  438. fun base64DecodeToByteArray(str: String?): ByteArray? {
  439. if (str.isNullOrBlank()) {
  440. return null
  441. }
  442. return EncoderUtils.base64DecodeToByteArray(str, 0)
  443. }
  444. fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? {
  445. if (str.isNullOrBlank()) {
  446. return null
  447. }
  448. return EncoderUtils.base64DecodeToByteArray(str, flags)
  449. }
  450. fun base64Encode(str: String): String? {
  451. return EncoderUtils.base64Encode(str, 2)
  452. }
  453. fun base64Encode(str: String, flags: Int): String? {
  454. return EncoderUtils.base64Encode(str, flags)
  455. }
  456. /* HexString 解码为字节数组 */
  457. fun hexDecodeToByteArray(hex: String): ByteArray? {
  458. return HexUtil.decodeHex(hex)
  459. }
  460. /* hexString 解码为utf8String*/
  461. fun hexDecodeToString(hex: String): String? {
  462. return HexUtil.decodeHexStr(hex)
  463. }
  464. /* utf8 编码为hexString */
  465. fun hexEncodeToString(utf8: String): String? {
  466. return HexUtil.encodeHexStr(utf8)
  467. }
  468. /**
  469. * 格式化时间
  470. */
  471. fun timeFormatUTC(time: Long, format: String, sh: Int): String? {
  472. val utc = SimpleTimeZone(sh, "UTC")
  473. return SimpleDateFormat(format, Locale.getDefault()).run {
  474. timeZone = utc
  475. format(Date(time))
  476. }
  477. }
  478. /**
  479. * 时间格式化
  480. */
  481. fun timeFormat(time: Long): String {
  482. return dateFormat.format(Date(time))
  483. }
  484. fun encodeURI(str: String): String {
  485. return try {
  486. URLEncoder.encode(str, "UTF-8")
  487. } catch (e: Exception) {
  488. ""
  489. }
  490. }
  491. fun encodeURI(str: String, enc: String): String {
  492. return try {
  493. URLEncoder.encode(str, enc)
  494. } catch (e: Exception) {
  495. ""
  496. }
  497. }
  498. fun htmlFormat(str: String): String {
  499. return HtmlFormatter.formatKeepImg(str)
  500. }
  501. fun t2s(text: String): String {
  502. return ChineseUtils.t2s(text)
  503. }
  504. fun s2t(text: String): String {
  505. return ChineseUtils.s2t(text)
  506. }
  507. fun getWebViewUA(): String {
  508. return WebSettings.getDefaultUserAgent(appCtx)
  509. }
  510. //****************文件操作******************//
  511. /**
  512. * 获取本地文件
  513. * @param path 相对路径
  514. * @return File
  515. */
  516. fun getFile(path: String): File {
  517. val cachePath = appCtx.externalCache.absolutePath
  518. val aPath = if (path.startsWith(File.separator)) {
  519. cachePath + path
  520. } else {
  521. cachePath + File.separator + path
  522. }
  523. val file = File(aPath)
  524. val safePath = appCtx.externalCache.parent!!
  525. if (!file.canonicalPath.startsWith(safePath)) {
  526. throw SecurityException("非法路径")
  527. }
  528. return file
  529. }
  530. fun readFile(path: String): ByteArray? {
  531. val file = getFile(path)
  532. if (file.exists()) {
  533. return file.readBytes()
  534. }
  535. return null
  536. }
  537. fun readTxtFile(path: String): String {
  538. val file = getFile(path)
  539. if (file.exists()) {
  540. val charsetName = EncodingDetect.getEncode(file)
  541. return String(file.readBytes(), charset(charsetName))
  542. }
  543. return ""
  544. }
  545. fun readTxtFile(path: String, charsetName: String): String {
  546. val file = getFile(path)
  547. if (file.exists()) {
  548. return String(file.readBytes(), charset(charsetName))
  549. }
  550. return ""
  551. }
  552. /**
  553. * 删除本地文件
  554. */
  555. fun deleteFile(path: String): Boolean {
  556. val file = getFile(path)
  557. return FileUtils.delete(file, true)
  558. }
  559. /**
  560. * js实现Zip压缩文件解压
  561. * @param zipPath 相对路径
  562. * @return 相对路径
  563. */
  564. fun unzipFile(zipPath: String): String {
  565. return unArchiveFile(zipPath)
  566. }
  567. /**
  568. * js实现7Zip压缩文件解压
  569. * @param zipPath 相对路径
  570. * @return 相对路径
  571. */
  572. fun un7zFile(zipPath: String): String {
  573. return unArchiveFile(zipPath)
  574. }
  575. /**
  576. * js实现Rar压缩文件解压
  577. * @param zipPath 相对路径
  578. * @return 相对路径
  579. */
  580. fun unrarFile(zipPath: String): String {
  581. return unArchiveFile(zipPath)
  582. }
  583. /**
  584. * js实现压缩文件解压
  585. * @param zipPath 相对路径
  586. * @return 相对路径
  587. */
  588. fun unArchiveFile(zipPath: String): String {
  589. if (zipPath.isEmpty()) return ""
  590. val zipFile = getFile(zipPath)
  591. return ArchiveUtils.deCompress(zipFile.absolutePath).let {
  592. ArchiveUtils.TEMP_FOLDER_NAME + File.separator + MD5Utils.md5Encode16(zipFile.name)
  593. }
  594. }
  595. /**
  596. * js实现文件夹内所有文本文件读取
  597. * @param path 文件夹相对路径
  598. * @return 所有文件字符串换行连接
  599. */
  600. fun getTxtInFolder(path: String): String {
  601. if (path.isEmpty()) return ""
  602. val folder = getFile(path)
  603. val contents = StringBuilder()
  604. folder.listFiles().let {
  605. if (it != null) {
  606. for (f in it) {
  607. val charsetName = EncodingDetect.getEncode(f)
  608. contents.append(String(f.readBytes(), charset(charsetName)))
  609. .append("\n")
  610. }
  611. contents.deleteCharAt(contents.length - 1)
  612. }
  613. }
  614. FileUtils.delete(folder.absolutePath)
  615. return contents.toString()
  616. }
  617. /**
  618. * 获取网络zip文件里面的数据
  619. * @param url zip文件的链接或十六进制字符串
  620. * @param path 所需获取文件在zip内的路径
  621. * @return zip指定文件的数据
  622. */
  623. fun getZipStringContent(url: String, path: String): String {
  624. val byteArray = getZipByteArrayContent(url, path) ?: return ""
  625. val charsetName = EncodingDetect.getEncode(byteArray)
  626. return String(byteArray, Charset.forName(charsetName))
  627. }
  628. fun getZipStringContent(url: String, path: String, charsetName: String): String {
  629. val byteArray = getZipByteArrayContent(url, path) ?: return ""
  630. return String(byteArray, Charset.forName(charsetName))
  631. }
  632. /**
  633. * 获取网络zip文件里面的数据
  634. * @param url zip文件的链接或十六进制字符串
  635. * @param path 所需获取文件在zip内的路径
  636. * @return zip指定文件的数据
  637. */
  638. fun getRarStringContent(url: String, path: String): String {
  639. val byteArray = getRarByteArrayContent(url, path) ?: return ""
  640. val charsetName = EncodingDetect.getEncode(byteArray)
  641. return String(byteArray, Charset.forName(charsetName))
  642. }
  643. fun getRarStringContent(url: String, path: String, charsetName: String): String {
  644. val byteArray = getRarByteArrayContent(url, path) ?: return ""
  645. return String(byteArray, Charset.forName(charsetName))
  646. }
  647. /**
  648. * 获取网络7zip文件里面的数据
  649. * @param url 7zip文件的链接或十六进制字符串
  650. * @param path 所需获取文件在7zip内的路径
  651. * @return zip指定文件的数据
  652. */
  653. fun get7zStringContent(url: String, path: String): String {
  654. val byteArray = get7zByteArrayContent(url, path) ?: return ""
  655. val charsetName = EncodingDetect.getEncode(byteArray)
  656. return String(byteArray, Charset.forName(charsetName))
  657. }
  658. fun get7zStringContent(url: String, path: String, charsetName: String): String {
  659. val byteArray = get7zByteArrayContent(url, path) ?: return ""
  660. return String(byteArray, Charset.forName(charsetName))
  661. }
  662. /**
  663. * 获取网络zip文件里面的数据
  664. * @param url zip文件的链接或十六进制字符串
  665. * @param path 所需获取文件在zip内的路径
  666. * @return zip指定文件的数据
  667. */
  668. fun getZipByteArrayContent(url: String, path: String): ByteArray? {
  669. val bytes = if (url.isAbsUrl()) {
  670. AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray()
  671. } else {
  672. HexUtil.decodeHex(url)
  673. }
  674. val bos = ByteArrayOutputStream()
  675. ZipInputStream(ByteArrayInputStream(bytes)).use { zis ->
  676. var entry: ZipEntry
  677. while (zis.nextEntry.also { entry = it } != null) {
  678. if (entry.name.equals(path)) {
  679. zis.use { it.copyTo(bos) }
  680. return bos.toByteArray()
  681. }
  682. entry = zis.nextEntry
  683. }
  684. }
  685. log("getZipContent 未发现内容")
  686. return null
  687. }
  688. /**
  689. * 获取网络Rar文件里面的数据
  690. * @param url Rar文件的链接或十六进制字符串
  691. * @param path 所需获取文件在Rar内的路径
  692. * @return Rar指定文件的数据
  693. */
  694. fun getRarByteArrayContent(url: String, path: String): ByteArray? {
  695. val bytes = if (url.isAbsUrl()) {
  696. AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray()
  697. } else {
  698. HexUtil.decodeHex(url)
  699. }
  700. return ByteArrayInputStream(bytes).use {
  701. LibArchiveUtils.getByteArrayContent(it, path)
  702. }
  703. }
  704. /**
  705. * 获取网络7zip文件里面的数据
  706. * @param url 7zip文件的链接或十六进制字符串
  707. * @param path 所需获取文件在7zip内的路径
  708. * @return 7zip指定文件的数据
  709. */
  710. fun get7zByteArrayContent(url: String, path: String): ByteArray? {
  711. val bytes = if (url.isAbsUrl()) {
  712. AnalyzeUrl(url, source = getSource(), coroutineContext = context).getByteArray()
  713. } else {
  714. HexUtil.decodeHex(url)
  715. }
  716. return ByteArrayInputStream(bytes).use {
  717. LibArchiveUtils.getByteArrayContent(it, path)
  718. }
  719. }
  720. //******************文件操作************************//
  721. /**
  722. * 解析字体Base64数据,返回字体解析类
  723. */
  724. @Deprecated(
  725. "Deprecated",
  726. ReplaceWith("queryTTF(data)")
  727. )
  728. fun queryBase64TTF(data: String?): QueryTTF? {
  729. log("queryBase64TTF(String)方法已过时,并将在未来删除;请无脑使用queryTTF(Any)替代,新方法支持传入 url、本地文件、base64、ByteArray 自动判断&自动缓存,特殊情况需禁用缓存请传入第二可选参数false:Boolean")
  730. return queryTTF(data)
  731. }
  732. /**
  733. * 返回字体解析类
  734. * @param data 支持url,本地文件,base64,ByteArray,自动判断,自动缓存
  735. * @param useCache 可选开关缓存,不传入该值默认开启缓存
  736. */
  737. @OptIn(ExperimentalStdlibApi::class)
  738. fun queryTTF(data: Any?, useCache: Boolean): QueryTTF? {
  739. try {
  740. var key: String? = null
  741. var qTTF: QueryTTF?
  742. when (data) {
  743. is String -> {
  744. if (useCache) {
  745. key = MessageDigest.getInstance("SHA-256").digest(data.toByteArray())
  746. .toHexString()
  747. qTTF = AppCacheManager.getQueryTTF(key)
  748. if (qTTF != null) return qTTF
  749. }
  750. val font: ByteArray? = when {
  751. data.isAbsUrl() -> AnalyzeUrl(
  752. data,
  753. source = getSource(),
  754. coroutineContext = context
  755. ).getByteArray()
  756. else -> base64DecodeToByteArray(data)
  757. }
  758. font ?: return null
  759. qTTF = QueryTTF(font)
  760. }
  761. is ByteArray -> {
  762. if (useCache) {
  763. key = MessageDigest.getInstance("SHA-256").digest(data).toHexString()
  764. qTTF = AppCacheManager.getQueryTTF(key)
  765. if (qTTF != null) return qTTF
  766. }
  767. qTTF = QueryTTF(data)
  768. }
  769. else -> return null
  770. }
  771. if (key != null) AppCacheManager.put(key, qTTF)
  772. return qTTF
  773. } catch (e: Exception) {
  774. AppLog.put("[queryTTF] 获取字体处理类出错", e)
  775. throw e
  776. }
  777. }
  778. fun queryTTF(data: Any?): QueryTTF? {
  779. return queryTTF(data, true)
  780. }
  781. /**
  782. * @param text 包含错误字体的内容
  783. * @param errorQueryTTF 错误的字体
  784. * @param correctQueryTTF 正确的字体
  785. * @param filter 删除 errorQueryTTF 中不存在的字符
  786. */
  787. fun replaceFont(
  788. text: String,
  789. errorQueryTTF: QueryTTF?,
  790. correctQueryTTF: QueryTTF?,
  791. filter: Boolean
  792. ): String {
  793. if (errorQueryTTF == null || correctQueryTTF == null) return text
  794. val contentArray = text.toStringArray() //这里不能用toCharArray,因为有些文字占多个字节
  795. val intArray = IntArray(1)
  796. contentArray.forEachIndexed { index, s ->
  797. val oldCode = s.codePointAt(0)
  798. // 忽略正常的空白字符
  799. if (errorQueryTTF.isBlankUnicode(oldCode)) {
  800. return@forEachIndexed
  801. }
  802. // 删除轮廓数据不存在的字符
  803. var glyf = errorQueryTTF.getGlyfByUnicode(oldCode) // 轮廓数据不存在
  804. if (errorQueryTTF.getGlyfIdByUnicode(oldCode) == 0) glyf = null // 轮廓数据指向保留索引0
  805. if (filter && (glyf == null)) {
  806. contentArray[index] = ""
  807. return@forEachIndexed
  808. }
  809. // 使用轮廓数据反查Unicode
  810. val code = correctQueryTTF.getUnicodeByGlyf(glyf)
  811. if (code != 0) {
  812. intArray[0] = code
  813. contentArray[index] = String(intArray, 0, 1)
  814. }
  815. }
  816. return contentArray.joinToString("")
  817. }
  818. /**
  819. * @param text 包含错误字体的内容
  820. * @param errorQueryTTF 错误的字体
  821. * @param correctQueryTTF 正确的字体
  822. */
  823. fun replaceFont(
  824. text: String,
  825. errorQueryTTF: QueryTTF?,
  826. correctQueryTTF: QueryTTF?
  827. ): String {
  828. return replaceFont(text, errorQueryTTF, correctQueryTTF, false)
  829. }
  830. /**
  831. * 章节数转数字
  832. */
  833. fun toNumChapter(s: String?): String? {
  834. s ?: return null
  835. val matcher = AppPattern.titleNumPattern.matcher(s)
  836. if (matcher.find()) {
  837. val intStr = StringUtils.stringToInt(matcher.group(2))
  838. return "${matcher.group(1)}${intStr}${matcher.group(3)}"
  839. }
  840. return s
  841. }
  842. fun toURL(urlStr: String): JsURL {
  843. return JsURL(urlStr)
  844. }
  845. fun toURL(url: String, baseUrl: String? = null): JsURL {
  846. return JsURL(url, baseUrl)
  847. }
  848. /**
  849. * 弹窗提示
  850. */
  851. fun toast(msg: Any?) {
  852. rhinoContext.ensureActive()
  853. appCtx.toastForJs("${getSource()?.getTag()}: ${msg.toString()}")
  854. }
  855. /**
  856. * 弹窗提示 停留时间较长
  857. */
  858. fun longToast(msg: Any?) {
  859. rhinoContext.ensureActive()
  860. appCtx.longToastForJs("${getSource()?.getTag()}: ${msg.toString()}")
  861. }
  862. /**
  863. * 输出调试日志
  864. */
  865. fun log(msg: Any?): Any? {
  866. rhinoContextOrNull?.ensureActive()
  867. getSource()?.let {
  868. Debug.log(it.getKey(), msg.toString())
  869. } ?: Debug.log(msg.toString())
  870. AppLog.putDebug("${getSource()?.getTag() ?: "源"}调试输出: $msg")
  871. return msg
  872. }
  873. /**
  874. * 输出对象类型
  875. */
  876. fun logType(any: Any?) {
  877. if (any == null) {
  878. log("null")
  879. } else {
  880. log(any.javaClass.name)
  881. }
  882. }
  883. /**
  884. * 生成UUID
  885. */
  886. fun randomUUID(): String {
  887. return UUID.randomUUID().toString()
  888. }
  889. fun androidId(): String {
  890. return AppConst.androidId
  891. }
  892. fun openUrl(url: String) {
  893. openUrl(url, null)
  894. }
  895. // 新增 mimeType 参数,默认为 null(保持兼容性)
  896. fun openUrl(url: String, mimeType: String? = null) {
  897. rhinoContext.ensureActive()
  898. val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null")
  899. appCtx.startActivity<OpenUrlConfirmActivity> {
  900. putExtra("uri", url)
  901. putExtra("mimeType", mimeType)
  902. putExtra("sourceOrigin", source.getKey())
  903. putExtra("sourceName", source.getTag())
  904. putExtra("sourceType", source.getSourceType())
  905. }
  906. }
  907. }