package chamelon

  1. Overview
  2. Docs

Source file block.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
(* a block is, physically, a revision count and series of commits *)
(* it can also have a hardtail, pointing at another blockpair where
 * the directory continues *)

module IdSet = Set.Make(Int)

type t = {
  revision_count : int;
  commits : Commit.t list;
  hardtail : Entry.t option;
}

type write_result = [ `Ok | `Split | `Split_emergency ]

let commits t = t.commits
let revision_count t = t.revision_count

let entries t = List.(flatten @@ map Commit.entries t.commits)

let crc_of_revision_count revision_count =
  let start_crc = Checkseum.Crc32.default in
  let cs = Cstruct.create 4 in
  let sizeof_crc = 4 in
  Cstruct.LE.set_uint32 cs 0 (Int32.of_int revision_count);
  let revision_count_crc = Checkseum.Crc32.(digest_bigstring
                             (Cstruct.to_bigarray cs) 0 sizeof_crc start_crc)
  in
  (* hey hey, ho ho, we don't want no overflow *)
  Optint.((logand) revision_count_crc @@ of_unsigned_int32 0xffffffffl)

let linked_blocks t =
  (* we call `compact` on the entry list because otherwise we'd incorrectly follow
   * deleted entries *)
  List.filter_map Entry.links @@ Entry.compact @@ entries t

let of_commits ~hardtail ~revision_count commits =
  (* we have to redo the crc for the first commit when the revision count changes :( *)
  (* we don't have to recalculate CRCs for any subsequent commits because the only data
   * dependency on a previous commit for later commits in a block
   * is on the last tag of the previous commit, which is the CRC tag.
   * The CRC tag's variable fields are its length and chunk value only,
   * and changes to the revision count are changes to a fixed-width field that
   * doesn't affect the chunk. *)
  match commits with
  | [] -> { commits; revision_count; hardtail}
  | commit :: l ->
    let crc = crc_of_revision_count revision_count in
    let new_commit = Commit.(of_entries_filter_crc (seed_tag commit) crc (entries commit)) in
    {commits = (new_commit :: l); revision_count; hardtail}

let of_entries ~revision_count entries =
  let crc = crc_of_revision_count revision_count in
  let commit = Commit.of_entries_filter_crc (Cstruct.of_string "\xff\xff\xff\xff") crc entries in
  of_commits ~hardtail:None ~revision_count (commit::[])

let compact t =
  let revision_count = t.revision_count + 1 in
  let entries = List.map Commit.entries t.commits |> List.flatten |> Entry.compact in
  of_entries ~revision_count entries

let add_commit {revision_count; commits; hardtail} entries =
  let revision_count = revision_count + 1 in
  match commits with
  | [] -> of_entries ~revision_count entries
  | l ->
    let last = List.(nth l ((length l) - 1)) in
    let commit = Commit.commit_after last entries in
    of_commits ~hardtail ~revision_count (l @ [commit])

let hardtail t =
  match t.hardtail with
  | None -> None
  | Some e -> Dir.hard_tail_links e

(* return variants `Split and `Ok are successful; `Split warns that the block
 * took up more than 1/2 of the block size and the metadata block
 * should be split into two pairs.
 *
 * `Split_emergency means the size of `block` exceeds the block size,
 * and the data *cannot* be written. A partial serialization remains in [cs] in
 * this case. *)
let into_cstruct ~program_block_size cs block =
  (* the hardtail is handled specially since its presence alters our next_commit_valid
   * and all entries need to be before it. *)
  let write_hardtail ~after ~starting_xor_tag ~starting_offset t cs =
    match t.hardtail with
    | None -> (0, starting_xor_tag)
    | Some entry ->
      let commit = Commit.commit_after after [entry] in
      Commit.into_cstruct ~filter_hardtail:false ~starting_offset ~program_block_size ~starting_xor_tag
        ~next_commit_valid:false cs commit
  in
  (* if there's nothing to write, just return *)
  match block.commits, block.hardtail with
  | [], None -> `Ok
  | commits, _ ->
    Cstruct.LE.set_uint32 cs 0 (Int32.of_int block.revision_count);
    try
      let after_last_crc, starting_xor_tag, starting_offset =
        List.fold_left
          (fun (pointer, prev_commit_last_tag, starting_offset) commit ->
             (* it may be a bit surprising that we don't use `last_tag` from `commit` as the previous tag here.
              * as serialized, the last tag in the commit is the tag for the CRC, so we need to XOR with that
              * rather than the last non-CRC tag, which is what's represented in `commit.last_tag`. *)
             let this_commit_region = Cstruct.shift cs pointer in
             let (bytes_written, raw_crc_tag) = Commit.into_cstruct ~filter_hardtail:true ~next_commit_valid:true
                 ~program_block_size ~starting_xor_tag:prev_commit_last_tag ~starting_offset
                 this_commit_region commit in
             (* only the first commit has nonzero offset; all subsequent ones have an offset of 0,
              * since each commit is padded to a multiple of the program block size. *)
             (pointer + bytes_written, raw_crc_tag, 0)
          ) (4, (Cstruct.of_string "\xff\xff\xff\xff"), 4) block.commits
      in
      let hardtail_region = Cstruct.shift cs after_last_crc in
      let hardtail_bytes, _raw_crc =
        write_hardtail ~after:List.(hd @@ rev commits) ~starting_xor_tag ~starting_offset block hardtail_region
      in
      if (after_last_crc + hardtail_bytes) > ((Cstruct.length cs) / 2) then `Split else `Ok
    with
    | Invalid_argument _ -> `Split_emergency

let ids t =
  let id_of_entry e = (fst e).Tag.id in
  let commit_ids c = List.map id_of_entry (Commit.entries c) in
  let block_ids = List.(flatten @@ map commit_ids t.commits) in
  IdSet.of_list block_ids

let split block next_blockpair =
  let entry = Dir.hard_tail_at next_blockpair in
  {block with hardtail = Some entry},
  of_entries ~revision_count:1 []

let to_cstruct ~program_block_size ~block_size block =
  let cs = Cstruct.create block_size in
  let res = into_cstruct ~program_block_size cs block in
  cs, res

let of_cstruct ~program_block_size cs =
  if Cstruct.length cs <= Tag.size
  then Error (`Msg "block is too small to contain littlefs commits")
  else begin
    let revision_count = Cstruct.LE.get_uint32 cs 0 |> Int32.to_int in
    let revision_count_crc = crc_of_revision_count revision_count in
    let commit_list = Cstruct.shift cs 4 in
    let starting_xor_tag = Cstruct.of_string "\xff\xff\xff\xff" in
    (* the first commit is at an offset of 4, because the revision count is hanging out at the front of the block *)
    let commits = Commit.of_cstructv ~preceding_crc:revision_count_crc
        ~starting_offset:4 ~starting_xor_tag ~program_block_size
        commit_list
    in
    let entries = List.(flatten @@ map Commit.entries commits) in
    let hardtail = List.find_opt (fun (t, _d) -> Tag.is_hardtail t) entries in
    Ok {revision_count; commits; hardtail}
  end
OCaml

Innovation. Community. Security.