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.go
のcheckVersion
にする。
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
を見ると、関連しているmessage
とservice
が定義されている。
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.go
のConfig構造体に列挙されている。
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
タグをつけてスキップする。
typoキーの検出
typoキーは、BurntSushi/toml#MetaData.Undecodedを用いて検出できる。
上記の設定ファイルの例では、次のように検出できている。ソートしているのは後の処理のため。
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
を別途調べる必要がある。
候補の判定
agext/levenshteinで、レーベンシュタイン距離を用いて判定する。
Terraformでも使われている。
結果の表示
TOMLの形式がおかしいなど致命的なエラーは赤、typoの警告は黄で表示する。
\x1b[31m *** \x1b[0m
のように文字列で指定するとWindowsでうまく表示できないのでfatih/colorを使用した。
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)のoptions
にwithFileTypes: true
を指定すると、fs.Direntが返ってくる。
fs.Dirent
のdirent.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 valuesにAssigned a default value in case the unpacked value is undefined.
と書いてあることや、13 ECMAScript Language: Expressionsの13.15.5.3 Runtime Semantics: PropertyDestructuringAssignmentEvaluationに4. 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 - offset
でbuffer = 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+FEFF
のU+
は、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"
。
そして重要なのがデフォルトがfalse
のTextDecoder.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が生成される例がある。