4 Commits 227ac4a248 ... ad75c2402e

Author SHA1 Message Date
  secext2022 ad75c2402e 添加(pmbb-iso): PMBB_SORT, PMBB_DEBUG 3 months ago
  secext2022 9a6d9c0194 添加(tar-sha256): cat 1.tar | ./tar-sha256 - 3 months ago
  secext2022 73df6931c5 添加(tar-sha256): 使用 pm-bin 3 months ago
  secext2022 d13a915e02 添加(命令): pmbb-iso 3 months ago
10 changed files with 407 additions and 59 deletions
  1. 22 0
      README.md
  2. 0 0
      doc/env.md
  3. 13 1
      src/bb/conf.ts
  4. 325 0
      src/bb/iso/parse.ts
  5. 7 1
      src/bb/mod.ts
  6. 34 0
      src/bin/pmbb-iso.ts
  7. 4 6
      tar-sha256/Cargo.toml
  8. 2 29
      tar-sha256/build.rs
  9. 0 22
      tar-sha256/src/bin.rs
  10. 0 0
      tar-sha256/src/calc.rs

+ 22 - 0
README.md

@@ -16,6 +16,28 @@
 - <https://notabug.org/fm-elpac/pmbb>
 - <https://gitlab.com/fm-elpac/pmbb>
 
+## 相关文章
+
+- 《胖喵贪吃: 备份数据文件的小工具》
+  - <https://zhuanlan.zhihu.com/p/709721640>
+  - <https://juejin.cn/post/7392898553234554914>
+  - <https://blog.csdn.net/secext2022/article/details/140558881>
+
+- 《光盘文件系统 (iso9660) 格式解析》
+  - <https://zhuanlan.zhihu.com/p/711411492>
+  - <https://juejin.cn/post/7396248017022402623>
+  - <https://blog.csdn.net/secext2022/article/details/140758509>
+
+- 《光驱的内部结构及日常使用》
+  - <https://zhuanlan.zhihu.com/p/709093480>
+  - <https://juejin.cn/post/7392066482974556170>
+  - <https://blog.csdn.net/secext2022/article/details/140558507>
+
+- 《光盘防水嘛 ? DVD+R 刻录光盘泡水实验》
+  - <https://zhuanlan.zhihu.com/p/709978759>
+  - <https://juejin.cn/post/7393313322235904052>
+  - <https://blog.csdn.net/secext2022/article/details/140583910>
+
 TODO
 
 ## LICENSE

+ 0 - 0
doc/env.md


+ 13 - 1
src/bb/conf.ts

@@ -5,7 +5,7 @@
 /**
  * 程序版本号
  */
-export const P_VERSION = "pmbb v0.1.0-a1";
+export const P_VERSION = "pmbb v0.1.0-a3";
 
 // 环境变量
 
@@ -18,3 +18,15 @@ export const ENV_PMBB_BS = "PMBB_BS";
  * pmbb-box: 装箱总数
  */
 export const ENV_PMBB_BN = "PMBB_BN";
+
+/**
+ * pmbb 调试输出
+ *
+ * pmbb-iso ls
+ */
+export const ENV_PMBB_DEBUG = "PMBB_DEBUG";
+
+/**
+ * pmbb-iso ls 结果排序
+ */
+export const ENV_PMBB_SORT = "PMBB_SORT";

+ 325 - 0
src/bb/iso/parse.ts

@@ -0,0 +1,325 @@
+// 解析 iso9660 文件列表.
+//
+// 参考资料: <https://wiki.osdev.org/ISO_9660>
+
+import { 显示大小 } from "../size.ts";
+
+// 光盘扇区大小
+export const 扇区 = 2048;
+
+// 读取文件的一部分数据
+async function 读文件(
+  f: Deno.FsFile,
+  偏移: number,
+  长度: number,
+): Promise<[Uint8Array, number | null]> {
+  await f.seek(偏移, Deno.SeekMode.Start);
+  const b = new Uint8Array(长度);
+  return [b, await f.read(b)];
+}
+
+// 读取一个光盘扇区
+//
+// 编号: 扇区编号
+async function 读扇区(f: Deno.FsFile, 编号: number): Promise<Uint8Array> {
+  const r = await 读文件(f, 编号 * 扇区, 扇区);
+  // TODO 检查读取失败
+  return r[0];
+}
+
+// 读取从某个扇区开始的数据
+async function 读数据(
+  f: Deno.FsFile,
+  编号: number,
+  长度: number,
+): Promise<Uint8Array> {
+  const r = await 读文件(f, 编号 * 扇区, 长度);
+  // TODO 检查读取失败
+  return r[0];
+}
+
+// 读取数据块的指定字节, 转换为文本
+function 读文本(数据: Uint8Array, 偏移: number, 长度: number): string {
+  const b = 数据.slice(偏移, 偏移 + 长度);
+  const d = new TextDecoder();
+  return d.decode(b);
+}
+
+// Joliet: UCS-2
+function 读文本2(数据: Uint8Array, 偏移: number, 长度: number): string {
+  const b = 数据.slice(偏移, 偏移 + 长度);
+  const d = new TextDecoder("utf-16be");
+  return d.decode(b);
+}
+
+function 读文本_2(
+  数据: Uint8Array,
+  偏移: number,
+  长度: number,
+  joliet: boolean = false,
+): string {
+  return joliet ? 读文本2(数据, 偏移, 长度) : 读文本(数据, 偏移, 长度);
+}
+
+export const 文件标志_目录 = 2;
+
+// Directory entry, directory record
+export interface 目录项 {
+  // Length of Directory Record
+  长度: number;
+  // Extended Attribute Record length
+  扩展属性长度: number;
+  // Location of extent (LBA)
+  位置: number;
+  // Data length (size of extent)
+  数据长度: number;
+
+  // File flags
+  文件标志: number;
+  _目录: boolean;
+
+  // File unit size for files recorded in interleaved mode
+  交错模式文件单元大小: number;
+  // Interleave gap size for files recorded in interleaved mode
+  交错模式文件间隔大小: number;
+
+  // Volume sequence number
+  卷序号: number;
+  // Length of file identifier (file name)
+  文件名长度: number;
+  // File identifier
+  文件名: string;
+  // 原始文件名
+  _文件名?: Uint8Array;
+  // 标记 . 和 .. 目录
+  _?: boolean;
+}
+
+function 解析目录项(b: Uint8Array, joliet: boolean = false): 目录项 {
+  const v = new DataView(b.buffer);
+  const 文件标志 = b[25];
+  const 文件名长度 = b[32];
+  const 文件名 = 读文本_2(b, 33, 文件名长度, joliet);
+  const _文件名 = b.slice(33, 33 + 文件名长度);
+
+  return {
+    长度: b[0],
+    扩展属性长度: b[1],
+    位置: v.getUint32(2, true),
+    数据长度: v.getUint32(10, true),
+
+    文件标志,
+    _目录: (文件标志 & 文件标志_目录) != 0,
+
+    交错模式文件单元大小: b[26],
+    交错模式文件间隔大小: b[27],
+    卷序号: v.getUint16(28, true),
+    文件名长度,
+    文件名,
+    _文件名,
+    // 检查 . 和 .. 目录
+    _: (1 == 文件名长度) && ((0 == _文件名[0]) || (1 == _文件名[0])),
+  };
+}
+
+// Primary Volume Descriptor
+export interface 主卷描述符 {
+  // System Identifier
+  系统标识: string;
+  // Volume Identifier
+  卷标: string;
+  // Volume Space Size
+  卷空间块: number;
+  // Volume Set Size
+  逻辑卷集大小: number;
+  // Volume Sequence Number
+  逻辑卷集序号: number;
+  // Logical Block Size
+  逻辑块大小: number;
+
+  // Directory entry for the root directory
+  根目录: 目录项;
+}
+
+// Boot Record
+export interface 启动记录 {
+  // Boot System Identifier
+  启动系统标识: string;
+  // Boot Identifier
+  启动标识: string;
+}
+
+// Volume Descriptor
+export interface 卷描述符 {
+  // Type
+  类型: number;
+  // Identifier
+  标识: string;
+  // Version
+  版本: number;
+
+  主?: 主卷描述符;
+  启动?: 启动记录;
+}
+
+// 卷描述符类型代码 Volume Descriptor Type Codes
+// Boot Record
+export const 卷描述符类型_启动记录 = 0;
+// Primary Volume Descriptor
+export const 卷描述符类型_主卷描述符 = 1;
+// Supplementary Volume Descriptor
+export const 卷描述符类型_次卷描述符 = 2;
+// Volume Partition Descriptor
+export const 卷描述符类型_卷分区描述符 = 3;
+// Volume Descriptor Set Terminator
+export const 卷描述符类型_结束 = 255;
+
+// 解析 Volume Descriptor
+function 解析卷描述符(b: Uint8Array): 卷描述符 {
+  const v = new DataView(b.buffer);
+  const o: 卷描述符 = {
+    类型: b[0],
+    标识: 读文本(b, 1, 5),
+    版本: b[6],
+  };
+  const joliet = 卷描述符类型_次卷描述符 == o.类型;
+
+  switch (o.类型) {
+    case 卷描述符类型_主卷描述符:
+    case 卷描述符类型_次卷描述符:
+      {
+        o.主 = {
+          系统标识: 读文本_2(b, 8, 32, joliet),
+          卷标: 读文本_2(b, 40, 32, joliet),
+          卷空间块: v.getUint32(80, true),
+          逻辑卷集大小: v.getUint16(120, true),
+          逻辑卷集序号: v.getUint16(124, true),
+          逻辑块大小: v.getUint16(128, true),
+
+          根目录: 解析目录项(b.slice(156, 156 + 34), joliet),
+        };
+      }
+      break;
+    case 卷描述符类型_启动记录:
+      o.启动 = {
+        启动系统标识: 读文本(b, 7, 32),
+        启动标识: 读文本(b, 39, 32),
+      };
+      break;
+  }
+  return o;
+}
+
+export interface 文件项 {
+  // 完整文件路径
+  路径: string;
+  // 扇区编号
+  位置: number;
+  // 文件长度 (字节)
+  长度: number;
+}
+
+// 递归遍历目录
+async function 遍历目录(
+  f: Deno.FsFile,
+  上级: 目录项,
+  路径: string,
+  o: Array<文件项>,
+) {
+  // 防止死循环: 跳过 . 和 .. 目录
+  if (上级._) {
+    return;
+  }
+  const p = 路径 + (上级._目录 ? "/" : "");
+  // 保存结果
+  o.push({
+    路径: p,
+    位置: 上级.位置,
+    长度: 上级.数据长度,
+  });
+  // 如果不是目录, 结束递归
+  if (!上级._目录) {
+    return;
+  }
+  //console.log(上级);
+
+  // 读取目录文件
+  const b = await 读数据(f, 上级.位置, 上级.数据长度);
+
+  // 当前目录项开始字节的位置
+  let i = 0;
+  // 循环解析每一个目录项
+  while (i < b.length) {
+    // 目录项长度
+    const 长度 = b[i];
+    // 单个目录项长度至少为 33 字节
+    if (长度 > 33) {
+      const 项 = 解析目录项(b.slice(i, i + 长度), true);
+      // 递归遍历
+      await 遍历目录(f, 项, 路径 + "/" + 项.文件名, o);
+    } else if (0 == 长度) {
+      // 跳过当前字节
+      i += 1;
+      continue;
+    } else {
+      // TODO
+      console.log("长度 = " + 长度);
+    }
+    // 读取下一个目录项
+    i += 长度;
+  }
+}
+
+// 输入: 光盘镜像文件 (iso)
+export async function 解析iso(
+  文件名: string,
+  debug: boolean = false,
+): Promise<Array<文件项>> {
+  // 打开光盘镜像文件
+  const f = await Deno.open(文件名);
+
+  // 解析卷描述符, 从 16 扇区开始
+  let vdi = 16;
+  // 保存根目录
+  let 根目录: 目录项 | undefined;
+
+  while (true) {
+    const 扇区 = await 读扇区(f, vdi);
+    const vd = 解析卷描述符(扇区);
+    if (debug) {
+      console.log(vdi, vd);
+    }
+
+    if (卷描述符类型_结束 == vd.类型) {
+      break;
+    } else if (卷描述符类型_次卷描述符 == vd.类型) {
+      根目录 = vd.主!.根目录;
+    }
+    // 继续读取下一个卷描述符
+    vdi += 1;
+  }
+
+  const o = [] as Array<文件项>;
+  if (null != 根目录) {
+    // 消除根目录标记
+    根目录._ = false;
+
+    if (debug) {
+      console.log("");
+    }
+    // 从根目录开始, 遍历目录树
+    await 遍历目录(f, 根目录, "", o);
+  }
+  return o;
+}
+
+// 按照在光盘上的起始位置 (扇区编号) 排序
+export function 结果排序(o: Array<文件项>) {
+  o.sort((a, b) => a.位置 - b.位置);
+}
+
+// 输出扇区编号 (数据长度) 和路径
+export function 显示结果(i: 文件项) {
+  const 大小 = "(" + 显示大小(i.长度) + " " + i.长度 + ")";
+  console.log(i.位置, 大小, i.路径);
+}

+ 7 - 1
src/bb/mod.ts

@@ -2,7 +2,13 @@
  * 胖喵贪吃 (PMBB) 主要代码 (库)
  */
 
-export { ENV_PMBB_BN, ENV_PMBB_BS, P_VERSION } from "./conf.ts";
+export {
+  ENV_PMBB_BN,
+  ENV_PMBB_BS,
+  ENV_PMBB_DEBUG,
+  ENV_PMBB_SORT,
+  P_VERSION,
+} from "./conf.ts";
 
 export { log1 } from "./log.ts";
 

+ 34 - 0
src/bin/pmbb-iso.ts

@@ -0,0 +1,34 @@
+/**
+ * pmbb-iso: 处理 iso9660 文件系统.
+ *
+ * 命令行参数: 命令 参数
+ * 栗子:
+ *
+ * deno run -A pmbb-iso.ts ls 1.iso
+ */
+
+import { ENV_PMBB_DEBUG, ENV_PMBB_SORT, log1, P_VERSION } from "../bb/mod.ts";
+import { 显示结果, 结果排序, 解析iso } from "../bb/iso/parse.ts";
+
+export async function pmbb_iso(a: Array<string>) {
+  const debug = Deno.env.get(ENV_PMBB_DEBUG) == 1 as unknown as string;
+  const 命令 = a[0];
+  if (debug) {
+    log1("pmbb-iso: " + P_VERSION);
+  }
+
+  if ("ls" == 命令) {
+    const 结果 = await 解析iso(a[1], debug);
+    if (Deno.env.get(ENV_PMBB_SORT) == 1 as unknown as string) {
+      结果排序(结果);
+    }
+    结果.forEach(显示结果);
+  } else {
+    log1("错误: 未知命令 " + 命令);
+    throw new Error("unknown command");
+  }
+}
+
+if (import.meta.main) {
+  pmbb_iso(Deno.args);
+}

+ 4 - 6
tar-sha256/Cargo.toml

@@ -1,6 +1,6 @@
 [package]
 name = "tar-sha256"
-version = "0.1.0-a1"
+version = "0.1.0-a2"
 edition = "2021"
 license = "MIT"
 
@@ -11,16 +11,14 @@ keywords = ["tar", "sha256"]
 categories = ["command-line-utilities", "filesystem"]
 
 [dependencies]
+pm-bin = "^0.1.0-a1"
+
 tar = "^0.4.41"
 sha2 = "^0.10.8"
 base16ct = "^0.2.0"
 
-log = "^0.4.22"
-env_logger = "^0.11.5"
-
 [build-dependencies]
-built = { version = "^0.7.4" }
-vergen-gitcl = { version = "^1.0.0", features = ["build"] }
+pm-bin = { version = "^0.1.0-a1", features = ["build"] }
 
 [features]
 default = ["base16ct/alloc"]

+ 2 - 29
tar-sha256/build.rs

@@ -1,33 +1,6 @@
+use pm_bin::build_gen;
 use std::error::Error;
-use std::path::PathBuf;
-
-use built;
-use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder};
 
 fn main() -> Result<(), Box<dyn Error>> {
-    // 每次编译都重新运行 `build.rs`
-    Emitter::default()
-        .add_instructions(&BuildBuilder::all_build()?)?
-        .add_instructions(
-            &GitclBuilder::default()
-                .describe(true, false, None)
-                .sha(false)
-                .build()?,
-        )?
-        .emit()?;
-
-    // `.git/index`
-    match PathBuf::from("../.git/index").canonicalize() {
-        Ok(p) => {
-            println!("cargo:rerun-if-changed={}", p.to_str().unwrap());
-        }
-        _ => {
-            println!("cargo:warning=can not find ../.git/index");
-        }
-    }
-
-    // 收集编译信息
-    built::write_built_file()?;
-
-    Ok(())
+    build_gen(Some("..".into()))
 }

+ 0 - 22
tar-sha256/src/bin.rs

@@ -1,22 +0,0 @@
-use log::debug;
-
-// 编译信息
-mod built_info {
-    include!(concat!(env!("OUT_DIR"), "/built.rs"));
-}
-
-/// 显示版本信息
-pub fn 版本() {
-    let name = env!("CARGO_PKG_NAME");
-    let v = env!("CARGO_PKG_VERSION");
-    let target = built_info::TARGET;
-    let features = built_info::FEATURES_LOWERCASE_STR;
-    println!("{} version {} ({}, {})", name, v, target, features);
-
-    // debug
-    let git = env!("VERGEN_GIT_DESCRIBE");
-    let profile = built_info::PROFILE;
-    let time = env!("VERGEN_BUILD_TIMESTAMP");
-    let rustc = built_info::RUSTC_VERSION;
-    debug!("{} {} {}, {}", git, profile, time, rustc);
-}

+ 0 - 0
tar-sha256/src/calc.rs


Some files were not shown because too many files changed in this diff