moonldif

    LDIF byte-preserving records and offline structural checks in MoonBit

    ldif
    ldap
    parser
    validation
    Download zip
    Author
    Version
    0.4.0
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    19

    #MoonLDIF

    MoonBit 原生 LDIF 读写与离线结构预检库。

    版本:0.4.0,新增迁移前后快照核对。正式发布及安装复验状态以 交付状态 为准;0.3.0 已发布基线继续保留。报名初审已通过,个人验收和组委会最终结果仍分别待确认。

    范围:LDIF 内容与基本变更记录、字节属性、源位置、确定性写回和结构报告。目录 Schema、DN 语义相等、权限及真实服务器执行结果不在检查范围内。

    已有 hbYlj/moonldap 提供 LDAP 网络与操作模型。本项目补充 LDIF 文件层,不宣称 LDAP 协议能力为原创。依据 RFC 2849 与官方勘误实现,独立参考工具只用于验证。

    许可:Apache-2.0。

    #现在能做什么

    • 读取目录导出的 LDIF 内容文件,包括中文、二进制属性、多值属性、空值与折行。
    • 解析新增、删除、修改、重命名计划;给出错误代码与物理行号,保留修改顺序。
    • 检查 DN/RDN 转义和组成,定位原始行;旧分隔空格需显式兼容,名称不自动改写。
    • 可分别启用 --deny-delete--deny-clear--deny-rename,拦截整条删除、属性清空、改名与移动;错误或不完整结果优先返回 2。
    • 安全整理为新文件:写出后重新读取比较,保留属性字节和操作顺序,拒绝覆盖已有文件。
    • 可选接入 hbYlj/moonldap 0.3.0 的操作模型,已做离线 BER 编解码往返测试。
    • 审阅删除指定值、删除整个属性、清空、替换、移动和控制项;保留操作顺序与位置,不展开属性值。
    • 在浏览器中打开文件、定位问题、编辑复检和导出新文件;处理留在本机,过期结果禁止下载;完成的 0/1/2 结果可下载审阅报告,只有 0 可导出整理后的 LDIF。

    核心解析、规则、写回、报告和参数判断都用 MoonBit 实现。Node.js 只负责本地文件操作、参数传输、源字节 SHA-256 和进程退出;浏览器负责文件、SHA-256 和下载。规则和报告正文由 MoonBit 生成。Python 只在独立验证或下载测试依赖时使用。

    #核对迁移前后的导出

    node dist/moonldif.js compare examples/snapshots/before.ldif examples/snapshots/after.ldif --format markdown

    预期退出 1,定位缺失条目、缺失邮箱、成员值减少和新增条目。将 after.ldif 换成 reordered.ldif 返回 0,忽略顺序和编码表示变化。浏览器点击“迁移前后核对”可体验同一核心。两侧输入的行号和 SHA-256 随报告保存,不附加属性原值。比较合同、库 API 与边界。compare 的 1 表示发现差异;2 表示核对不完整,优先于 1。

    #一次检查多份文件

    node dist/moonldif.js batch examples/01-directory-export.ldif examples/02-account-changes.ldif examples/03-migration-plan.ldif --deny-delete --deny-clear --deny-rename --format json

    此合成例预期返回 1。任一文件错误/不完整优先返回 2,并继续保留其他文件风险。报告包含文件基本名、DN、源字节指纹和逐文件结果;不会读取外部 URL。最多 50 份、单份 8 MiB、累计 32 MiB。详见 批量说明与库 API可复用 CI 示例

    #安装 MoonBit 库

    在自己的 MoonBit 工程执行 moon add WeiR-h/moonldif@0.4.0,并在 moon.pkg 导入 "WeiR-h/moonldif" @ldif。注册表安装验证使用 python scripts/registry-verify.py --version 0.4.0,创建没有本地覆盖的独立消费工程。

    安装 MoonBit 库不会安装 Node.js CLI。CLI 使用下文的源码构建方式;浏览器工作台另按以下步骤启动。

    #浏览器试用

    公开入口:MoonLDIF 工作台。只部署通过双平台与浏览器 CI 的正式版本;当前在线版本为 0.3.0,公开 Chromium/Firefox 流程已验证。页面提供整条删除、属性清空、改名与移动、不完整输入四类合成示例。

    从项目根目录执行以下命令,再打开 http://127.0.0.1:4178/

    npm --prefix web ci npm --prefix web run build npm --prefix web run preview

    默认合成示例展示删除拦截。点击“定位原文”,修订内容后重新检查;检查完成且策略允许时导出新文件。浏览器入口上限 1 MiB / 10,000 行。详见 工作台说明浏览器验证说明。React 仅负责界面,分析和写回使用同一个 MoonBit 核心。

    #本机立即试用

    首次使用请先按下文“从源码构建与验收”克隆并构建。然后在项目文件夹执行:

    node scripts/demo.mjs

    会依次演示目录导出检查、账号删除拦截、迁移计划检查。第二个场景出现退出码 1 是预期拦截,演示脚本会判断是否符合预期。三个文件均为人工构造,不是真实企业案例。

    node dist/moonldif.js check examples/01-directory-export.ldif node dist/moonldif.js inspect examples/03-migration-plan.ldif --format json node dist/moonldif.js review examples/02-account-changes.ldif --deny-delete --deny-clear --format markdown node dist/moonldif.js check examples/02-account-changes.ldif --deny-delete node dist/moonldif.js format examples/01-directory-export.ldif --output normalized.ldif

    最后一个命令要求 normalized.ldif 尚不存在。路径包含空格时使用引号。inspect 会显示属性内容;普通 check 只显示统计和诊断。

    退出码解释
    0支持范围内检查完成,启用的策略未拦截
    1检查完成,启用的风险策略拦截
    2输入或运行错误,或存在未能完整分析的结构/外部值;优先于 1

    检查通过不等于服务器能成功导入。 DN/RDN 字符串语法已检查;目录 Schema、名称匹配语义、权限、服务端状态和控制语义未检查。URL 引用绝不读取,会使结果为 incomplete。详见 支持矩阵与边界

    #从源码构建与验收

    需要 Node.js 24 和官方 MoonBit 工具链。本轮验证版本为 moonc v0.10.11+6ff76a5f9,其他工具链版本暂未验证。新机器按 MoonBit 官方工具链说明 安装,并将 moon 加入 PATH,或设置 MOON_HOME

    git clone https://github.com/WeiR-h/moonldif.git cd moonldif npm run build npm test npm run verify

    核心与 CLI 无需安装 npm 依赖;浏览器工作台的依赖由 web/package-lock.json 固定。verify 包含格式、类型、JS / Wasm GC 两个目标的 49 个核心测试、构建、16 组 CLI 集成测试和三个场景;实际输出与时间记录在 verification/local/远端双平台 CI 已执行通过,另包含工作台构建、归档公共 API、独立参考和生态适配验证;对应提交和完整证据见 公开交付记录

    本机使用忽略提交的 .local-toolchain.json 指向已有 MoonBit 工具链;它不是项目源代码依赖。换机器时安装工具链即可,不需要 MoonAPI Check 工程。

    #独立对照与生态适配

    需要 Python 3.10+。首次运行会下载固定版本的公开参考源码/包归档并检查 SHA-256;都存于忽略提交的本地目录,不会打开 LDIF 中的 URL。

    python scripts/reference-verify.py python scripts/snapshot-verify.py python scripts/openldap-verify.py python scripts/sdk-verify.py python scripts/rfc-verify.py python scripts/package-verify.py node scripts/benchmark.mjs python scripts/prepare-moonldap.py node scripts/test-moonldap.mjs

    Python 对照覆盖七组内容数据和一组 modify 顺序;新增 Java/JDK 17+ 的 SDK 对照覆盖四类变更等 27 组输入,其中一组的 SDK 输出需显式转换扩展语法,其余 26 组三个方向直接通过。七个 RFC 示例分别记录原刊与修订结果,外部值不记为完整通过。详见 互操作证据规范覆盖表。moonldap 适配另有三个测试:四种实际模型及 BER 往返、拒绝不支持的输入、拒绝新增风险策略拦截的报告。适配是单独的本地工作区,普通核心构建不下载这些依赖。

    #MoonBit 库接口

    dev.4 增加 OpenLDAP 原始测试样本验证:六份固定文件不做修订,四份在显式 --compat 下通过独立 Python 三方向对照,两份保留拒绝和写出阻断。此验证发现并修正了未知修改操作被错误解释成替换的审阅问题;不代表支持全部 OpenLDAP 输入或服务器导入。

    mooncakes 模块为 WeiR-h/moonldif。默认按上面的指定版本从注册表安装;可选 MoonLDAP 适配工作区单独提供实际示例。

    package-verify.py 将实际发行归档解压到新工作区,以公共 API 在 JS/Wasm GC 两个目标验证字节写回、操作审阅、策略和不完整状态。它没有从注册表下载,不代替 registry-verify.py 的注册表安装验收。

    let report = @ldif.check_text("version: 1\ndn: cn=Demo\ncn: Demo\n")
    println(report.to_text())
    if report.exit_code() == 0 {
    let normalized = report.format() // 可抛出 WriteError
    println(normalized)
    }

    公共接口包括 check(Bytes)check_text(String)parse_dn(String)parse_rdn(String)parse(Bytes)parse_text(String)write(Document)Report::format()、文本/JSON 报告及 document_jsonparse 只解析结构;check 额外检查名称。旧 DN 分隔空格可单独启用 --legacy-dn-spaces 并产生警告,详见 名称检查。完整签名见 公共 API。属性原始值用 Bytes 存储;源位置与诊断可直接访问。

    #项目资料

    WriteError

    pub suberror WriteError {
    InvalidDocument(String)
    } derive(
    Debug
    )

    Attribute

    pub(all) struct Attribute {
    name : String
    value : Value
    span : Span
    } derive(Eq)

    BatchReview

    pub struct BatchReview {
    // private fields
    }

    Bounded, ordered offline review. No cross-file execution or LDAP state is inferred. Only compact reports are retained, not input bytes or parsed documents.

    BatchReview::add_bytes

    fn BatchReview::add_bytes(self : BatchReview, label : String, data : Bytes, input_sha256 : String) -> Unit

    The host must compute input_sha256 from data. Syntax is checked here; this identity is not an authenticity proof. Labels are display names, not paths.

    BatchReview::add_unavailable

    fn BatchReview::add_unavailable(self : BatchReview, label : String) -> Unit

    Record a host I/O failure without leaking an OS error or local path.

    BatchReview::exit_code

    fn BatchReview::exit_code(self : BatchReview) -> Int

    BatchReview::new

    fn BatchReview::new(options? : Options, risk_policy? : RiskPolicy, legacy_dn_spaces? : Bool) -> BatchReview

    BatchReview::remaining_bytes

    fn BatchReview::remaining_bytes(self : BatchReview) -> Int

    Remaining cumulative accepted-input-byte budget, for bounded host I/O.

    BatchReview::to_json

    fn BatchReview::to_json(self : BatchReview) -> Json

    BatchReview::to_markdown

    fn BatchReview::to_markdown(self : BatchReview) -> String

    BatchReview::to_text

    fn BatchReview::to_text(self : BatchReview) -> String

    Body

    pub(all) enum Body {
    Entry(Array[Attribute])
    Add(Array[Attribute])
    Delete
    Modify(Array[Modification])
    Rename(String, Bool, String?)
    } derive(Eq)

    Control

    pub(all) struct Control {
    oid : String
    critical : Bool
    value : Value?
    span : Span
    } derive(Eq)

    Diagnostic

    pub(all) struct Diagnostic {
    code : String
    severity : String
    span : Span
    reason : String
    } derive(Eq, ToJson)

    DistinguishedName

    pub(all) struct DistinguishedName {
    rdns : Array[RelativeName]
    } derive(Eq)

    Document

    pub(all) struct Document {
    mode : String
    records : Array[Record]
    } derive(Eq)

    Modification

    pub(all) struct Modification {
    operation : String
    attribute : String
    values : Array[Attribute]
    span : Span
    } derive(Eq)

    NameAssertion

    pub(all) struct NameAssertion {
    attribute : String
    value : NameValue
    byte_offset : Int
    } derive(Eq)

    NameError

    pub(all) struct NameError {
    code : String
    byte_offset : Int
    reason : String
    } derive(Eq, ToJson)

    NameValue

    pub(all) enum NameValue {
    Text(Bytes)
    Ber(Bytes)
    } derive(Eq)

    Decoded assertion bytes or opaque BER hex form. No directory schema or distinguishedNameMatch is applied by this syntax parser.

    Options

    pub(all) struct Options {
    allow_missing_version : Bool
    deny_delete : Bool
    } derive(Eq)

    Options::default

    fn Options::default() -> Options

    Record

    pub(all) struct Record {
    dn : String
    controls : Array[Control]
    body : Body
    span : Span
    } derive(Eq)

    RelativeName

    pub(all) struct RelativeName {
    assertions : Array[NameAssertion]
    } derive(Eq)

    Report

    pub(all) struct Report {
    document : Document
    diagnostics : Array[Diagnostic]
    names_checked : Bool
    }

    Report::exit_code

    fn Report::exit_code(self : Report) -> Int

    Report::format

    fn Report::format(self : Report) -> String raise WriteError

    Report::not_checked

    fn Report::not_checked(self : Report) -> Array[String]

    Report::review

    fn Report::review(self : Report) -> Review

    File-level operation intent only. Review never executes LDAP or approves an import, never changes parser/policy status, and exposes no attribute values. Item order follows records and operations. The first 200 items are retained.

    Report::review_json

    fn Report::review_json(self : Report, metadata : ReviewMetadata) -> Json

    Additive fields preserve the existing report JSON shape. Attribute bytes, complete input text, filenames and host paths are intentionally omitted.

    Report::review_markdown

    fn Report::review_markdown(self : Report, metadata : ReviewMetadata) -> String

    Report::status

    fn Report::status(self : Report) -> String

    Report::to_json

    fn Report::to_json(self : Report, include_records? : Bool, include_review? : Bool) -> Json

    Report::to_text

    fn Report::to_text(self : Report, include_records? : Bool) -> String

    Review

    pub(all) struct Review {
    items : Array[ReviewItem]
    total_items : Int
    truncated : Bool
    analysis_complete : Bool
    input_exit_code : Int
    record_count : Int
    } derive(Eq)

    Review::to_json

    fn Review::to_json(self : Review) -> Json

    Review::to_text

    fn Review::to_text(self : Review) -> String

    ReviewItem

    pub(all) struct ReviewItem {
    code : String
    level : String
    record_index : Int
    dn : String
    attribute : String?
    value_count : Int
    span : Span
    title : String
    reason : String
    action : String
    } derive(Eq)

    ReviewMetadata

    pub(all) struct ReviewMetadata {
    input_sha256 : String
    input_byte_length : Int
    options : Options
    legacy_dn_spaces : Bool
    risk_policy : RiskPolicy
    } derive(Eq)

    Host-computed identity of the exact analysed bytes, not a signature. Callers must supply the options used to create the report.

    RiskPolicy

    pub(all) struct RiskPolicy {
    deny_clear : Bool
    deny_rename : Bool
    } derive(Eq, ToJson)

    Opt-in file-intent checks. These do not query directory state or schema.

    RiskPolicy::default

    fn RiskPolicy::default() -> RiskPolicy

    SnapshotDiff

    pub struct SnapshotDiff {
    // private fields
    }

    A bounded comparison of two content exports, not an executable LDAP patch.

    SnapshotDiff::exit_code

    fn SnapshotDiff::exit_code(self : SnapshotDiff) -> Int

    SnapshotDiff::to_json

    fn SnapshotDiff::to_json(self : SnapshotDiff) -> Json

    SnapshotDiff::to_markdown

    fn SnapshotDiff::to_markdown(self : SnapshotDiff) -> String

    Human-readable summary and locations; all input-derived strings are escaped.

    SnapshotDiff::to_text

    fn SnapshotDiff::to_text(self : SnapshotDiff) -> String

    Human-readable reports retain all JSON fields without raw attribute bytes.

    Span

    pub(all) struct Span {
    line : Int
    end_line : Int
    } derive(Eq, ToJson)

    Value

    pub(all) enum Value {
    Inline(Bytes)
    External(String)
    } derive(Eq)

    check

    fn check(data : Bytes, options? : Options, legacy_dn_spaces? : Bool, risk_policy? : RiskPolicy) -> Report

    Structural parsing plus DN/newrdn/newsuperior string-syntax checks. Diagnostics locate original physical fields even with Base64 or folding.

    check_text

    fn check_text(text : String, options? : Options, legacy_dn_spaces? : Bool, risk_policy? : RiskPolicy) -> Report

    compare_snapshots

    fn compare_snapshots(before : Bytes, after : Bytes, allow_missing_version? : Bool, legacy_dn_spaces? : Bool, before_sha256? : String?, after_sha256? : String?) -> SnapshotDiff

    DN strings match exactly after LDIF decoding, without LDAP name matching. Attribute descriptions ignore ASCII case/option order. Values are byte multisets (duplicates counted); order, wrapping and Base64 spelling do not matter. Host-computed SHA-256 is optional and never an authenticity proof. 0 = no differences, 1 = differences, 2 = incomplete/invalid, with 2 priority.

    document_json

    fn document_json(document : Document) -> Json

    Semantic JSON omits source locations, but preserves record/operation/value order and the distinction between absent and empty control values.

    parse

    fn parse(data : Bytes, options? : Options) -> Report

    Parse a bounded LDIF v1 buffer. Diagnostics retain partial records for inspection; callers must check exit_code before accepting the document.

    parse_dn

    fn parse_dn(text : String) -> Result[DistinguishedName, NameError]

    RFC 4514 string grammar. Empty DN is the root; values are never trimmed.

    parse_rdn

    fn parse_rdn(text : String) -> Result[RelativeName, NameError]

    parse_text

    fn parse_text(text : String, options? : Options) -> Report

    same_attribute_description

    fn same_attribute_description(a : String, b : String) -> Bool

    LDAP attribute-description spelling only; does not compare DN or values.

    version

    fn version() -> String

    write

    fn write(document : Document) -> String raise WriteError

    Deterministic semantic serialization. Comments, casing of changetype aliases, original folding and source locations are not preserved. Refuses models that do not round-trip through this supported profile.