経験は何よりも饒舌

10年後に真価を発揮するかもしれないブログ 

M1Macで「ゼロからのOS自作入門」(みかん本)を完走した

day1~day30のログはここに。
scrapbox.io

環境構築はこの記事を参考にしたらできたが、
zenn.dev

このコメント通りに修正する必要があった。
https://zenn.dev/link/comments/3860a03795708b


day4で発生した/usr/include/stdint.h:26:10: fatal error: 'bits/libc-header-start.h' file not foundを解決するために、stdint.hを使わないようにした。
https://scrapbox.io/wafuwafuoss/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80#635c8a98b96bbf0000ab84b8
osbook_day30fでこの差分を元に戻しても起動できた。原因はよくわかってない。

vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/elf.hpp
diff --git a/kernel/elf.hpp b/kernel/elf.hpp
index 99ab80b..4f05cbc 100644
--- a/kernel/elf.hpp
+++ b/kernel/elf.hpp
@@ -1,14 +1,12 @@
 #pragma once
 
-#include <stdint.h>
-
-typedef uintptr_t Elf64_Addr;
-typedef uint64_t  Elf64_Off;
-typedef uint16_t  Elf64_Half;
-typedef uint32_t  Elf64_Word;
-typedef int32_t   Elf64_Sword;
-typedef uint64_t  Elf64_Xword;
-typedef int64_t   Elf64_Sxword;
+typedef unsigned long Elf64_Addr;
+typedef unsigned long  Elf64_Off;
+typedef unsigned char  Elf64_Half;
+typedef unsigned int  Elf64_Word;
+typedef int Elf64_Sword;
+typedef unsigned long  Elf64_Xword;
+typedef long Elf64_Sxword;
 
 #define EI_NIDENT 16
vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/frame_buffer_config.hpp 
diff --git a/kernel/frame_buffer_config.hpp b/kernel/frame_buffer_config.hpp
index 0ce8035..c9bb380 100644
--- a/kernel/frame_buffer_config.hpp
+++ b/kernel/frame_buffer_config.hpp
@@ -1,16 +1,14 @@
 #pragma once
 
-#include <stdint.h>
-
 enum PixelFormat {
   kPixelRGBResv8BitPerColor,
   kPixelBGRResv8BitPerColor,
 };
 
 struct FrameBufferConfig {
-  uint8_t* frame_buffer;
-  uint32_t pixels_per_scan_line;
-  uint32_t horizontal_resolution;
-  uint32_t vertical_resolution;
+  unsigned char* frame_buffer;
+  unsigned int pixels_per_scan_line;
+  unsigned int horizontal_resolution;
+  unsigned int vertical_resolution;
   enum PixelFormat pixel_format;
 };
vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/memory_map.hpp 
diff --git a/kernel/memory_map.hpp b/kernel/memory_map.hpp
index ef61348..e3b68d6 100644
--- a/kernel/memory_map.hpp
+++ b/kernel/memory_map.hpp
@@ -1,22 +1,20 @@
 #pragma once
 
-#include <stdint.h>
-
 struct MemoryMap {
   unsigned long long buffer_size;
   void* buffer;
   unsigned long long map_size;
   unsigned long long map_key;
   unsigned long long descriptor_size;
-  uint32_t descriptor_version;
+  unsigned int descriptor_version;
 };
 
 struct MemoryDescriptor {
-  uint32_t type;
-  uintptr_t physical_start;
-  uintptr_t virtual_start;
-  uint64_t number_of_pages;
-  uint64_t attribute;
+  unsigned int type;
+  unsigned long physical_start;
+  unsigned long virtual_start;
+  unsigned long number_of_pages;
+  unsigned long attribute;
 };
 
 #ifdef __cplusplus
@@ -39,11 +37,11 @@ enum class MemoryType {
   kEfiMaxMemoryType
 };
 
-inline bool operator==(uint32_t lhs, MemoryType rhs) {
-  return lhs == static_cast<uint32_t>(rhs);
+inline bool operator==(unsigned int lhs, MemoryType rhs) {
+  return lhs == static_cast<unsigned int>(rhs);
 }
 
-inline bool operator==(MemoryType lhs, uint32_t rhs) {
+inline bool operator==(MemoryType lhs, unsigned int rhs) {
   return rhs == lhs;
 }

osbook_day15dの「描画の高速化」をすると、Failed to allocate pages: NotFoundが出てしまったので、DrawAreaを使わないようにした。
https://scrapbox.io/wafuwafuoss/%E3%82%BC%E3%83%AD%E3%81%8B%E3%82%89%E3%81%AEOS%E8%87%AA%E4%BD%9C%E5%85%A5%E9%96%80#63801fb5b96bbf0000092981
osbook_day30fでこの差分を元に戻しても起動できた。原因はよくわかってない。

vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/message.hpp 
diff --git a/kernel/message.hpp b/kernel/message.hpp
index 8798623..148e82e 100644
--- a/kernel/message.hpp
+++ b/kernel/message.hpp
@@ -1,7 +1,7 @@
 #pragma once
 
 enum class LayerOperation {
-  Move, MoveRelative, Draw, DrawArea
+  Move, MoveRelative, Draw
 };
 
 struct Message {
@@ -37,7 +37,6 @@ struct Message {
       LayerOperation op;
       unsigned int layer_id;
       int x, y;
-      int w, h;
     } layer;
 
     struct {
vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/layer.hpp 
diff --git a/kernel/layer.hpp b/kernel/layer.hpp
index 5f616da..427ff29 100644
--- a/kernel/layer.hpp
+++ b/kernel/layer.hpp
@@ -69,8 +69,6 @@ class LayerManager {
   void Draw(const Rectangle<int>& area) const;
   /** @brief 指定したレイヤーに設定されているウィンドウの描画領域内を再描画する。 */
   void Draw(unsigned int id) const;
-  /** @brief 指定したレイヤーに設定されているウィンドウ内の指定された範囲を再描画する。 */
-  void Draw(unsigned int id, Rectangle<int> area) const;
 
   /** @brief レイヤーの位置情報を指定された絶対座標へと更新する。再描画する。 */
   void Move(unsigned int id, Vector2D<int> new_pos);
@@ -123,17 +121,5 @@ extern std::map<unsigned int, uint64_t>* layer_task_map;
 void InitializeLayer();
 void ProcessLayerMessage(const Message& msg);
 
-constexpr Message MakeLayerMessage(
-    uint64_t task_id, unsigned int layer_id,
-    LayerOperation op, const Rectangle<int>& area) {
-  Message msg{Message::kLayer, task_id};
-  msg.arg.layer.layer_id = layer_id;
-  msg.arg.layer.op = op;
-  msg.arg.layer.x = area.pos.x;
-  msg.arg.layer.y = area.pos.y;
-  msg.arg.layer.w = area.size.x;
-  msg.arg.layer.h = area.size.y;
-  return msg;
-}
 
 Error CloseLayer(unsigned int layer_id);
vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/layer.cpp
diff --git a/kernel/layer.cpp b/kernel/layer.cpp
index 1188b71..f4d3978 100644
--- a/kernel/layer.cpp
+++ b/kernel/layer.cpp
@@ -89,20 +89,12 @@ void LayerManager::Draw(const Rectangle<int>& area) const {
 }
 
 void LayerManager::Draw(unsigned int id) const {
-  Draw(id, {{0, 0}, {-1, -1}});
-}
-
-void LayerManager::Draw(unsigned int id, Rectangle<int> area) const {
   bool draw = false;
   Rectangle<int> window_area;
   for (auto layer : layer_stack_) {
     if (layer->ID() == id) {
       window_area.size = layer->GetWindow()->Size();
       window_area.pos = layer->GetPosition();
-      if (area.size.x >= 0 || area.size.y >= 0) {
-        area.pos = area.pos + window_area.pos;
-        window_area = window_area & area;
-      }
       draw = true;
     }
     if (draw) {
@@ -304,9 +296,6 @@ void ProcessLayerMessage(const Message& msg) {
   case LayerOperation::Draw:
     layer_manager->Draw(arg.layer_id);
     break;
-  case LayerOperation::DrawArea:
-    layer_manager->Draw(arg.layer_id, {{arg.x, arg.y}, {arg.w, arg.h}});
-    break;
   }
 }
vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/terminal.cpp 
diff --git a/kernel/terminal.cpp b/kernel/terminal.cpp
index 8762f72..1f4665d 100644
diff --git a/kernel/terminal.cpp b/kernel/terminal.cpp
index 8762f72..1f4665d 100644
--- a/kernel/terminal.cpp
+++ b/kernel/terminal.cpp
@@ -639,27 +639,18 @@ void Terminal::Print(const char* s, std::optional<size_t> len) {
   }
 
   DrawCursor(true);
-  const auto cursor_after = CalcCursorPos();
-
-  Vector2D<int> draw_pos{ToplevelWindow::kTopLeftMargin.x, cursor_before.y};
-  Vector2D<int> draw_size{window_->InnerSize().x,
-                          cursor_after.y - cursor_before.y + 16};
-
-  Rectangle<int> draw_area{draw_pos, draw_size};
-
-  Message msg = MakeLayerMessage(
-      task_.ID(), LayerID(), LayerOperation::DrawArea, draw_area);
+  Message msg{Message::kLayer, task_.ID()};
+  msg.arg.layer.layer_id = LayerID();
+  msg.arg.layer.op = LayerOperation::Draw;
   __asm__("cli");
   task_manager->SendMessage(1, msg);
   __asm__("sti");
 }
 
 void Terminal::Redraw() {
-  Rectangle<int> draw_area{ToplevelWindow::kTopLeftMargin,
-                           window_->InnerSize()};
-
-  Message msg = MakeLayerMessage(
-      task_.ID(), LayerID(), LayerOperation::DrawArea, draw_area);
+  Message msg{Message::kLayer, task_.ID()};
+  msg.arg.layer.layer_id = LayerID();
+  msg.arg.layer.op = LayerOperation::Draw;
   __asm__("cli");
   task_manager->SendMessage(1, msg);
   __asm__("sti");
@@ -743,9 +734,10 @@ void TaskTerminal(uint64_t task_id, int64_t data) {
     case Message::kTimerTimeout:
       add_blink_timer(msg->arg.timer.timeout);
       if (show_window && window_isactive) {
-        const auto area = terminal->BlinkCursor();
-        Message msg = MakeLayerMessage(
-            task_id, terminal->LayerID(), LayerOperation::DrawArea, area);
+        terminal->BlinkCursor();
+        Message msg{Message::kLayer, task_id};
+        msg.arg.layer.layer_id = terminal->LayerID();
+        msg.arg.layer.op = LayerOperation::Draw;
         __asm__("cli");
         task_manager->SendMessage(1, msg);
         __asm__("sti");
@@ -757,8 +749,9 @@ void TaskTerminal(uint64_t task_id, int64_t data) {
                                              msg->arg.keyboard.keycode,
                                              msg->arg.keyboard.ascii);
         if (show_window) {
-          Message msg = MakeLayerMessage(
-              task_id, terminal->LayerID(), LayerOperation::DrawArea, area);
+          Message msg{Message::kLayer, task_id};
+          msg.arg.layer.layer_id = terminal->LayerID();
+          msg.arg.layer.op = LayerOperation::Draw;
           __asm__("cli");
           task_manager->SendMessage(1, msg);
           __asm__("sti");

osbook_day24aの「ターミナルを増やす」でF2キーが分からなかったのでZにした。

vscode ➜ /workspaces/mikanos-devcontainer/mikanos (tags/osbook_day30f ✗) $ git diff kernel/main.cpp 
diff --git a/kernel/main.cpp b/kernel/main.cpp
index 40e67ed..e483fcf 100644
--- a/kernel/main.cpp
+++ b/kernel/main.cpp
@@ -211,7 +211,7 @@ extern "C" void KernelMainNewStack(
           InputTextWindow(msg->arg.keyboard.ascii);
         }
       } else if (msg->arg.keyboard.press &&
-                 msg->arg.keyboard.keycode == 59 /* F2 */) {
+                 msg->arg.keyboard.keycode == 29 /* Z */) {
         task_manager->NewTask()
           .InitContext(TaskTerminal, 0)
           .Wakeup();


自分の場合、順番に進めるにあたって変更する箇所ができたが、osbook_day30fでは差分なしで起動できた。素晴らしいコンテナのおかげでMacでも問題なく開発できる。
意味がわからず進めていた箇所が多いのでこれから改造して理解を進めていきたい。

etcdのgRPCコードリーディング

Do'er Advent Calendar 2022の22日目です。

etcdとは「a strongly consistent, distributed key-value store that provides a reliable way to store data that needs to be accessed by a distributed system or cluster of machines」であり、KubernetesやRookで使われていて、Quickstartを参考にすれば手元で動かせます。

いくつかコミットをしていますが、etcdではgRPCが使われているのでいろいろと思い出す必要がありました。
2年前くらいにやったThe complete gRPC courseをもう一度やったらなんとなく読めるようになったのでメモ書きをします。



コードリーディングの対象は命名からして簡単そうなclient.gocheckVersionにする。
Statusの返り値であるresp.Versionを使って処理がされているので、これがどこから来た何なのかを知りたい。

etcd/client.go at 16e1fff519eeff66e626dd15fef399ea2b10b9cc · etcd-io/etcd · GitHub

func (c *Client) checkVersion() (err error) {
...
   resp, rerr := c.Status(ctx, e)
   ...
   vs := strings.Split(resp.Version, ".")
...

Statusの定義ジャンプをすると、maintenance.goに飛ぶ。
etcd/maintenance.go at 16e1fff519eeff66e626dd15fef399ea2b10b9cc · etcd-io/etcd · GitHub

func (m *maintenance) Status(ctx context.Context, endpoint string) (*StatusResponse, error) {
...
   resp, err := remote.Status(ctx, &pb.StatusRequest{}, m.callOpts...)
...

さらにStatusの定義ジャンプをすると、proto定義から生成されるrpc.pb.goに飛ぶ。
etcd/rpc.pb.go at 16e1fff519eeff66e626dd15fef399ea2b10b9cc · etcd-io/etcd · GitHub

type MaintenanceClient interface {
	// Alarm activates, deactivates, and queries alarms regarding cluster health.
	Alarm(ctx context.Context, in *AlarmRequest, opts ...grpc.CallOption) (*AlarmResponse, error)
	// Status gets the status of the member.
	Status(ctx context.Context, in *StatusRequest, opts ...grpc.CallOption) (*StatusResponse, error)
...

rpc.protoを見ると、関連しているmessageserviceが定義されている。
StatusResponseには目的のversionがある。

etcd/rpc.proto at 16e1fff519eeff66e626dd15fef399ea2b10b9cc · etcd-io/etcd · GitHub

message StatusRequest {
  option (versionpb.etcd_version_msg) = "3.0";
}

message StatusResponse {
  option (versionpb.etcd_version_msg) = "3.0";

  ResponseHeader header = 1;
  // version is the cluster protocol version used by the responding member.
  string version = 2;
...
}

service Maintenance {
...
  // Status gets the status of the member.
  rpc Status(StatusRequest) returns (StatusResponse) {
      option (google.api.http) = {
        post: "/v3/maintenance/status"
        body: "*"
    };
  }

RESTっぽいパスが定義されているし、scripts/genproto.sh--grpc-gateway_outがあったのでgRPC-Gatewayを使っていそう。だが完全に忘れているのでエンドポイントの関数がどこにあるのかわからない。Statusでコード検索をかけるとserver/etcdserver/api/v3rpc/maintenance.goという、いかにもそれっぽいパスに関数が定義されていた。
定数のversion.Versionが返り値であるrespに含まれている。

func (ms *maintenanceServer) Status(ctx context.Context, ar *pb.StatusRequest) (*pb.StatusResponse, error) {
	...
	resp := &pb.StatusResponse{
		Header:           hdr,
		Version:          version.Version,

...

という感じでなんとなく読めたのでおしまい。

mackerel-agent configtest によるチェック強化の実装について

Mackerel Advent Calendar 2022 15日目です。

mackerel-agent configtestによるチェック機能を強化したので実装を軽く紹介します。
github.com
mackerel.io

何が変わったのか

次のようなmackerel-agent-sample.confを作成し、mackerel-agent configtestを実行する。

apikey = "abcdefg"
podfile = "/path/to/pidfile"

[foo]
command = "bar"
 
[plugin.checks.incorrect1]
command = "test command"
action = { command = "test command", user = "test user", en = { TEST_KEY = "VALUE_1" } }

[plugins.checks.incorrect2]
command = "test command"

[plugin.check.incorrect3]
command = "test command"

強化前は、Syntax OKとなる。

$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf
/usr/local/etc/mackerel-agent-sample.conf Syntax OK

強化後は、各設定項目typoで無効になっていたら警告を出してくれる。

$ mackerel-agent configtest -conf /usr/local/etc/mackerel-agent-sample.conf
[WARNING] foo is unexpected key. Did you mean root ?
[WARNING] plugin.check.incorrect3 is unexpected key. Did you mean plugin.checks.incorrect3 ?
[WARNING] plugin.checks.incorrect1.action.en is unexpected key. Did you mean plugin.checks.incorrect1.action.env ?
[WARNING] plugins is unexpected key. Did you mean plugin ?
[WARNING] podfile is unexpected key. Did you mean pidfile ?

実装

候補の列挙

候補とは、Did you mean ******のことで、各設定項目に該当する。
コードではconfig/config.goConfig構造体に列挙されている。

type Config struct {
	Apibase       string
        ...
        DisplayName   string        `toml:"display_name"`
        ...
        HostStatus    HostStatus    `toml:"host_status" conf:"parent"`
        ...
        HostIDStorage   HostIDStorage   `conf:"ignore"`
        ...
}
  • Apibaseのように、タグがついていない場合、フィールド名の先頭を小文字にして追加する。
  • DisplayNameのように、tomlタグがついている場合、その値を追加する。
  • HostStatusのように、フィールドの型が構造体の場合、parentタグをつけて候補を再帰的に取得する(ここだとon_start, on_stop)。
  • HostIDStorageのように、設定ファイルに書き込まれることがない場合、ignoreタグをつけてスキップする。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

typoキーの検出

typoキーは、BurntSushi/toml#MetaData.Undecodedを用いて検出できる。

上記の設定ファイルの例では、次のように検出できている。ソートしているのは後の処理のため。

mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

func ValidateConfigFile(file string) ([]UnexpectedKey, error) {
    config := &Config{}
    md, err := toml.DecodeFile(file, config)
    ...
    undecodedKeys := md.Undecoded()
    sort.Slice(undecodedKeys, func(i, j int) bool {
	return undecodedKeys[i].String() < undecodedKeys[j].String()
    })

    // [foo foo.command
    //  plugin.checks.incorrect1.action.en plugin.checks.incorrect1.action.en.TEST_KEY
    //  plugins.checks.incorrect2 plugins.checks.incorrect2.command
    //  podfile]
    fmt.Printf("%+v", undecodedKeys)
typoキーの判定
  • fooの場合、fooは候補として列挙されていないので、typoキーはfoo
  • plugin.checks.incorrect1.action.enの場合、pluginが候補として列挙されているので、typoキーはen
  • plugins.checks.incorrect2の場合、pluginsは候補として列挙されていないので、typoキーはplugins
  • podfileの場合、podfileは候補として列挙されていないので、typoキーはpodfile
  • foo.commandは、fooが既に判定されているのでスキップ。
  • plugin.checks.incorrect1.action.en.TEST_KEYは、plugin.checks.incorrect1.action.enが既に判定されているのでスキップ。
  • plugins.checks.incorrect2.commandは、plugins.checks.incorrect2が既に判定されているのでスキップ。

スキップしないとContents of suggestion when executing configtest may not be correct · Issue #829 · mackerelio/mackerel-agent · GitHubのような不具合が生じてしまう。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub
Config構造体にparentタグがあるかで判定するより、候補として列挙されているかどうかで判定する方が良かったというPR:
refactor(config/validate): use `candidates` instead of `parentConfKeys` by wafuwafu13 · Pull Request #839 · mackerelio/mackerel-agent · GitHub

plugin.typo.fooの検出

プラグインの設定項目は、checks, metrics, metadataでないといけないが、Config構造体ではstringであれば良いので、MetaData.Undecodedでは検出できない。
上記の設定ファイルの例だと、plugin.check.incorrect3が検出されていない。

type Config struct {
   ...
   Plugin map[string]map[string]*PluginConfig `conf:"parent"`
   ....
}

そのため、config.Pluginを別途調べる必要がある。

実装: mackerel-agent/validate.go at 5a2668daac02a5e2a406d22ddd23af6cca32e7be · mackerelio/mackerel-agent · GitHub

結果の表示

TOMLの形式がおかしいなど致命的なエラーは赤、typoの警告は黄で表示する。
\x1b[31m *** \x1b[0mのように文字列で指定するとWindowsでうまく表示できないのでfatih/colorを使用した。

展望

今はtypoキーをソートして表示しているけど、余裕があれば設定ファイルの行数を取得してその順番通りにtypoの行数も表示したい。

Rust学習進捗2022

フロントエンド界隈のOSSで目立ってきたり、Linuxに取り込まれたり、放ってはおけない言語、それがRust。

1年前くらいに入門したこの本に再び目を通した。
gihyo.jp

実践で書いてみたくなったから awesome-alternatives-in-rust に目を通してテストが足りてなかったjqlにコミット。
github.com
github.com

Denoにもちょこっとコミット。
github.com
wafuwafu13.hatenadiary.com

自分でも何か作りたくなったから mkr の Rust実装 mkrust を作成。基本的なことを一通り終えたから開発が止まっている。
github.com
github.com

Cと比較されることが多いのでC版の mkrc を作成。
github.com

メモリ管理の良さを実感できなかったのでガベージコレクションアルゴリズムと実装を読んだ。
tatsu-zine.com

GCが搭載されてないといかに大変かがわかった本。
book.mynavi.jp


Do'er Advent Calendar 2022 の1日目でした。

deno/std/nodeのfs.DirentでisBlockDeviceの判定ができないのはRustに実装がされていないから...ではない

追記: Rustにis_block_deviceがあって、Denoにはなかった
github.com


fs.readdir(path, options, callback)optionswithFileTypes: trueを指定すると、fs.Direntが返ってくる。
fs.Direntdirent.isBlockDevice()を使った例が以下。

$ node
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> const files = fs.readdirSync(".", {withFileTypes: true})
undefined
> files[0].isFile()
true
> files[0].isBlockDevice()
false

しかし、Deno(std/node)では使えない。

$ deno
Deno 1.25.4
exit using ctrl+d or close()
> import {readdirSync} from "https://deno.land/std@0.157.0/node/fs.ts"
undefined
> const files = readdirSync(".", {withFileTypes: true})
undefined
> files[0].isFile()
false
> files[0].isBlockDevice()
Uncaught Error: Not implemented: Deno does not yet support identification of block devices
    at notImplemented (https://deno.land/std@0.157.0/node/_utils.ts:23:9)
    at Dirent.isBlockDevice (https://deno.land/std@0.157.0/node/_fs/_fs_dirent.ts:8:5)
    at <anonymous>:2:10

deno_std/node/_fs/_fs_dirent.tsを見るとnotImplementedとされている。

export default class Dirent {
  constructor(private entry: Deno.DirEntry) {}

  isBlockDevice(): boolean {
    notImplemented("Deno does not yet support identification of block devices");
    return false;
  }
  ...

std/nodeのreaddirSyncは内部的にはDeno.readDirSyncが使われているのでDenoのコードを見にいくとそれっぽい箇所があった。
is_fileがあるから上記の例でfiles[0].isFile()が使えている。

  FsStat {
    is_file: metadata.is_file(),
    is_directory: metadata.is_dir(),
    is_symlink: metadata.file_type().is_symlink(),
...

metadataの参照先はRustなのでRustのコードを見にいくとそれっぽい箇所があった。

impl Metadata {
    #[must_use]
    #[stable(feature = "file_type", since = "1.1.0")]
    pub fn file_type(&self) -> FileType {
        FileType(self.0.file_type())
    }

    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn is_dir(&self) -> bool {
        self.file_type().is_dir()
    }

    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn is_file(&self) -> bool {
        self.file_type().is_file()
    }

    #[must_use]
    #[stable(feature = "is_symlink", since = "1.58.0")]
    pub fn is_symlink(&self) -> bool {
        self.file_type().is_symlink()
    }
...

ここにis_block_deviceを生やせば解決するはず...だけどメタ的な意味でRustわからん

JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ

「JSのオブジェクトの分割代入で既定値が割り当てられるのはundefinedの場合のみ」ということは、MDNのAssigning to new variable names and providing default valuesAssigned a default value in case the unpacked value is undefined.と書いてあることや、13 ECMAScript Language: Expressionsの13.15.5.3 Runtime Semantics: PropertyDestructuringAssignmentEvaluation4. If Initializeropt is present and v is undefined, thenとあることから分かる。

ここからNode.jsのfs.readを使ってみていく。

node/lib/fs.js#L605~ に以下のコードがある。

function read(fd, buffer, offsetOrOptions, length, position, callback) {
...
      } else if (arguments.length === 3) {
      // This is fs.read(fd, bufferOrParams, callback)
        if (!isArrayBufferView(buffer)) {
        // This is fs.read(fd, params, callback)
          params = buffer;
          ({ buffer = Buffer.alloc(16384) } = params ?? kEmptyObject);
        }
        ...
...
   ({
      offset = 0,
      length = buffer.byteLength - offset,
      position = null,
    } = params ?? kEmptyObject);
...
   validateBuffer(buffer);
...

例えばfs.read(fd, {offset: 1}, callback)というように呼び出すと、
params = {offset: 1}
({ buffer = Buffer.alloc(16384) } = {offset: 1}
buffer = Buffer.alloc(16384)
というように処理される。

もしfs.read(fd, {buffer: null}, callback)というように呼び出すと、
params = {buffer: null}
({ buffer = Buffer.alloc(16384) } = {buffer: null}
buffer = null
というように処理される。
この場合、validateBufferが呼びされる前にlength = buffer.byteLength - offsetbuffer = nullが参照されてしまう。

これを解決したのがこのPR。
github.com

ちなみにdeno_stdは現時点でNodeのv18.8.0との互換を保っているため、以下のように意図的にnullを参照するようにしている。
github.com

// @ts-ignore: Intentionally create TypeError for passing test-fs-read.js#L87
length = opt.buffer.byteLength;

RFC8259 の「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」について

プログラマのための文字コード技術入門』の p.216 にある、
JSONでは、データ先頭にBOMをつけないことが求められています(RFC8259)」
についてちょっとだけ詳しく調べる。

RFC8259の該当箇所は8.1. Character Encodingで、
「Implementations MUST NOT add a byte order mark (U+FEFF) to the beginning of a networked-transmitted JSON text」
とある。
続いて
「In the interests of interoperability, implementations that parse JSON texts MAY ignore the presence of a byte order mark rather than treating it as an error」
とあるように、相互運用性の観点から、BOMがあればエラーとして扱うのではなく、無視してもいいらしい。

BOM(Byte Order Mark)とは、ビッグエンディアンかリトルエンディアンのどちらのバイト順を採用しているかを示すために、データの先頭に付ける印のこと。
U+FEFFの符号位置を用い、ビッグエンディアンではFE FF、リトルエンディアンではFF FEという2バイトの列になる。
符号位置は、文字コード表の中の位置のこと。
ビッグエンディアンは、上位8ビットが先頭にくるバイト順、リトルエンディアンは、下位8ビットが先頭にくるバイト順のこと。
U+FEFFU+は、Unicodeの符号位置を表すのに付ける接頭辞。

16ビットのデータを8ビット単位のバイト列にするときには、ビッグエンディアンかリトルエンディアンのどちらを採用するかの問題がある。
Unicodeの符号化方式の中の1つであるUTF-16は、16ビット単位であるためこの問題が発生し、解消するためにBOMを付けることがある。

JSONでBOMを付けないとなると、バイト順の区別はどうするのだろうと思ったところ、同じ RFC8259 8.1. Character Encoding にこう書いてあった。
JSON text exchanged between systems that are not part of a closed ecosystem MUST be encoded using UTF-8
UTF-16を扱わないのにRFCなぜBOMに言及しているのだろうと思ったところ、『プログラマのための文字コード技術入門』の p.154 にこう書いてあった。
「バイト順が問題になることのないUTF-8にはBOMは本来関係がありません。ところが、BOMをUTF-8で表現した3バイトの値(EF BB BF)が、UTF-8のデータ列の先頭についていることがあります。バイト順の印という元々の意味を離れて、UTF-8のデータであることの印として利用価値があるという考えもあります。」

ネットワーク転送されるJSONにはその利用価値は必要なく、相互運用性に支障をきたすから、「Implementations MUST NOT add a byte order mark to the beginning of a networked-transmitted JSON text」とされている。



BOMに関する実際の問題の例として、node-fetchのHandling a BOM with .json() #541というissueにある、レスポンスをJSONとして取得する際にエラーが起こるという問題がある。
node-fetchの`.json()`には内部的に`JSON.parse`が用いられており、引数のtextにBOMが含まれていれば、そこでエラーが生じてしまう。JSONの構文はRFC8259に準拠しているからだ。
ユーザー側の解決例としてはここにあるように、textとして取得してから、BOMに当たる0xFEFFを除去し、JSONにする例がある。
node-fetch側のリリースされている解決法は、fix: handle bom in text and json #1482にあるように、内部でbuffer.toString()ではなくTextDecoder().decode()を使うようにしている。
buffer.toString()では、BOMに当たる0xFEFFがあったとしてもそのまま文字列化してしまい、それをJSON.parseするためエラーが生じる。
TextDecoder()のパラメータであるutfLabelのデフォルトは"utf-8"
そして重要なのがデフォルトがfalseTextDecoder.ignoreBOM
命名がややこしいが、TextDecoder.prototype.ignoreBOM not working as expectedの回答に
「If on the other hand you want it to be removed from the output, then leave it as false, the parser will treat it specially, and remove it from the output」
とあるように、TextDecoder().decode(buffer)の結果にはBOMが含まれなくなり、問題なくJSON.parse(text)ができる。


このように目に見えないBOMと戦ってJSONが生成される例がある。